Mengapa di Java 8 split terkadang menghapus string kosong di awal larik hasil?

110

Sebelum Java 8 ketika kita membagi string kosong seperti

String[] tokens = "abc".split("");

mekanisme perpecahan akan terpecah di tempat-tempat yang ditandai dengan |

|a|b|c|

karena ruang kosong ""ada sebelum dan sesudah setiap karakter. Jadi sebagai hasilnya itu akan menghasilkan array ini pada awalnya

["", "a", "b", "c", ""]

dan nanti akan menghapus sisa string kosong (karena kami tidak secara eksplisit memberikan nilai negatif ke limitargumen) sehingga akhirnya akan kembali

["", "a", "b", "c"]

Di Java 8 mekanisme split sepertinya telah berubah. Sekarang saat kami menggunakan

"abc".split("")

kita akan mendapatkan ["a", "b", "c"]larik alih-alih ["", "a", "b", "c"]jadi sepertinya string kosong di awal juga dihapus. Tetapi teori ini gagal karena misalnya

"abc".split("a")

mengembalikan array dengan string kosong di awal ["", "bc"].

Adakah yang bisa menjelaskan apa yang terjadi di sini dan bagaimana aturan pemisahan telah berubah di Java 8?

Pshemo
sumber
Java8 sepertinya memperbaikinya. Sementara itu, s.split("(?!^)")sepertinya berhasil.
shkschneider
2
Perilaku @shkschneider yang dijelaskan dalam pertanyaan saya bukanlah bug dari versi Java-8 sebelumnya. Perilaku ini tidak terlalu berguna, tetapi masih benar (seperti yang ditunjukkan dalam pertanyaan saya), jadi kami tidak dapat mengatakan bahwa itu "diperbaiki". Saya melihatnya lebih seperti perbaikan sehingga kita bisa menggunakan split("")daripada samar (untuk orang yang tidak menggunakan regex) split("(?!^)")atau split("(?<!^)")atau beberapa regex lainnya.
Pshemo
1
Mengalami masalah yang sama setelah mengupgrade fedora ke Fedora 21, fedora 21 dikirimkan dengan JDK 1.8, dan aplikasi game IRC saya rusak karena hal ini.
LiuYan 刘 研
7
Pertanyaan ini tampaknya menjadi satu-satunya dokumentasi dari perubahan yang mengganggu ini di Java 8. Oracle tidak memasukkannya dari daftar ketidakcocokan mereka .
Sean Van Gorder
4
Perubahan di JDK ini hanya membutuhkan waktu 2 jam untuk melacak apa yang salah. Kode berjalan dengan baik di komputer saya (JDK8) tetapi gagal secara misterius di komputer lain (JDK7). Oracle SEBENARNYA HARUS memperbarui dokumentasi String.split (String regex) , bukan di Pattern.split atau String.split (String regex, int limit) karena ini adalah penggunaan yang paling umum. Java dikenal dengan portabilitas alias yang disebut WORA. Ini adalah perubahan besar yang merusak dan tidak terdokumentasi dengan baik sama sekali.
PoweredByRice

Jawaban:

84

Perilaku String.split(yang memanggil Pattern.split) berubah antara Java 7 dan Java 8.

Dokumentasi

Membandingkan antara dokumentasi Pattern.splitdi Java 7 dan Java 8 , kami mengamati klausa berikut ditambahkan:

Jika ada kecocokan lebar-positif di awal urutan masukan, maka substring utama yang kosong disertakan di awal larik yang dihasilkan. Kecocokan lebar-nol di awal, namun tidak pernah menghasilkan substring depan yang kosong.

Klausul yang sama juga ditambahkan String.splitdi Java 8 , dibandingkan dengan Java 7 .

Implementasi referensi

Mari kita bandingkan kode Pattern.splitimplemetasi referensi di Java 7 dan Java 8. Kode tersebut diambil dari grepcode, untuk versi 7u40-b43 dan 8-b132.

Jawa 7

public String[] split(CharSequence input, int limit) {
    int index = 0;
    boolean matchLimited = limit > 0;
    ArrayList<String> matchList = new ArrayList<>();
    Matcher m = matcher(input);

    // Add segments before each match found
    while(m.find()) {
        if (!matchLimited || matchList.size() < limit - 1) {
            String match = input.subSequence(index, m.start()).toString();
            matchList.add(match);
            index = m.end();
        } else if (matchList.size() == limit - 1) { // last one
            String match = input.subSequence(index,
                                             input.length()).toString();
            matchList.add(match);
            index = m.end();
        }
    }

    // If no match was found, return this
    if (index == 0)
        return new String[] {input.toString()};

    // Add remaining segment
    if (!matchLimited || matchList.size() < limit)
        matchList.add(input.subSequence(index, input.length()).toString());

    // Construct result
    int resultSize = matchList.size();
    if (limit == 0)
        while (resultSize > 0 && matchList.get(resultSize-1).equals(""))
            resultSize--;
    String[] result = new String[resultSize];
    return matchList.subList(0, resultSize).toArray(result);
}

Jawa 8

public String[] split(CharSequence input, int limit) {
    int index = 0;
    boolean matchLimited = limit > 0;
    ArrayList<String> matchList = new ArrayList<>();
    Matcher m = matcher(input);

    // Add segments before each match found
    while(m.find()) {
        if (!matchLimited || matchList.size() < limit - 1) {
            if (index == 0 && index == m.start() && m.start() == m.end()) {
                // no empty leading substring included for zero-width match
                // at the beginning of the input char sequence.
                continue;
            }
            String match = input.subSequence(index, m.start()).toString();
            matchList.add(match);
            index = m.end();
        } else if (matchList.size() == limit - 1) { // last one
            String match = input.subSequence(index,
                                             input.length()).toString();
            matchList.add(match);
            index = m.end();
        }
    }

    // If no match was found, return this
    if (index == 0)
        return new String[] {input.toString()};

    // Add remaining segment
    if (!matchLimited || matchList.size() < limit)
        matchList.add(input.subSequence(index, input.length()).toString());

    // Construct result
    int resultSize = matchList.size();
    if (limit == 0)
        while (resultSize > 0 && matchList.get(resultSize-1).equals(""))
            resultSize--;
    String[] result = new String[resultSize];
    return matchList.subList(0, resultSize).toArray(result);
}

Penambahan kode berikut di Java 8 mengecualikan kecocokan panjang-nol di awal string input, yang menjelaskan perilaku di atas.

            if (index == 0 && index == m.start() && m.start() == m.end()) {
                // no empty leading substring included for zero-width match
                // at the beginning of the input char sequence.
                continue;
            }

Menjaga kompatibilitas

Mengikuti perilaku di Java 8 ke atas

Untuk membuat splitberperilaku secara konsisten di seluruh versi dan kompatibel dengan perilaku di Java 8:

  1. Jika ekspresi reguler Anda bisa cocok dengan string panjang-nol, cukup tambahkan (?!\A)di akhir ekspresi reguler dan bungkus ekspresi reguler asli dalam grup non-penangkap (?:...)(jika perlu).
  2. Jika ekspresi reguler Anda tidak cocok dengan string panjang-nol, Anda tidak perlu melakukan apa pun.
  3. Jika Anda tidak tahu apakah ekspresi reguler bisa cocok dengan string panjang-nol atau tidak, lakukan kedua tindakan di langkah 1.

(?!\A) memeriksa bahwa string tidak berakhir di awal string, yang berarti bahwa pertandingan tersebut adalah pertandingan kosong di awal string.

Mengikuti perilaku di Java 7 dan sebelumnya

Tidak ada solusi umum untuk membuat splitkompatibel dengan Java 7 dan sebelumnya, selain mengganti semua instance splituntuk mengarah ke implementasi kustom Anda sendiri.

nhahtdh
sumber
Tahu bagaimana saya dapat mengubah split("")kode sehingga konsisten di berbagai versi java?
Daniel
2
@Daniel: Dimungkinkan untuk membuatnya kompatibel ke depan (ikuti perilaku Java 8) dengan menambahkan (?!^)ke akhir regex dan membungkus regex asli dalam grup non-capturing (?:...)(jika perlu), tetapi saya tidak dapat memikirkan apa pun cara membuatnya kompatibel ke belakang (ikuti perilaku lama di Java 7 dan sebelumnya).
nhahtdh
Terima kasih untuk penjelasannya. Bisakah Anda menjelaskan "(?!^)"? Dalam skenario apa itu akan berbeda ""? (Saya buruk di regex!: - /).
Daniel
1
@Daniel: Artinya dipengaruhi oleh Pattern.MULTILINEbendera, sementara \Aselalu cocok di awal string apa pun benderanya.
nhahtdh
30

Ini telah ditentukan dalam dokumentasi split(String regex, limit).

Jika ada kecocokan lebar positif di awal string ini, substring utama yang kosong disertakan di awal larik yang dihasilkan. Kecocokan lebar-nol di awal, namun tidak pernah menghasilkan substring depan yang kosong.

Dalam "abc".split("")Anda punya pertandingan nol-lebar di awal sehingga substring kosong terkemuka tidak termasuk dalam array yang dihasilkan.

Namun dalam cuplikan kedua saat Anda berpisah, "a"Anda mendapatkan kecocokan lebar positif (1 dalam kasus ini), jadi substring utama yang kosong disertakan seperti yang diharapkan.

(Kode sumber yang tidak relevan dihapus)

Alexis C.
sumber
3
Itu hanya sebuah pertanyaan. Bolehkah memposting potongan kode dari JDK? Ingat masalah hak cipta dengan Google - Harry Potter - Oracle?
Paul Vargas
6
@PaulVargas Agar adil saya tidak tahu tetapi saya menganggap tidak apa-apa karena Anda dapat mengunduh JDK, dan membuka zip file src yang berisi semua sumber. Jadi secara teknis semua orang bisa melihat sumbernya.
Alexis C.
12
@PaulVargas "Open" dalam "open source" berarti sesuatu.
Marko Topolnik
2
@ZouZou: hanya karena semua orang dapat melihatnya, bukan berarti Anda dapat memublikasikannya kembali
pengguna102008
2
@Paul Vargas, IANAL tetapi dalam banyak kesempatan lain jenis posting ini termasuk dalam situasi kutipan / penggunaan wajar. Lebih lanjut tentang topik ada di sini: meta.stackexchange.com/questions/12527/…
Alex Pakka
14

Ada sedikit perubahan dalam dokumen split()dari Java 7 ke Java 8. Secara khusus, pernyataan berikut telah ditambahkan:

Jika ada kecocokan lebar positif di awal string ini, substring utama yang kosong disertakan di awal larik yang dihasilkan. Kecocokan lebar-nol di awal, namun tidak pernah menghasilkan substring depan yang kosong.

(penekanan saya)

Pemisahan string kosong menghasilkan kecocokan lebar-nol di awal, jadi string kosong tidak disertakan di awal larik yang dihasilkan sesuai dengan yang ditentukan di atas. Sebaliknya, contoh kedua Anda yang berpisah "a"menghasilkan kecocokan lebar- positif di awal string, jadi string kosong sebenarnya disertakan di awal larik yang dihasilkan.

arshajii
sumber
Beberapa detik lagi membuat perbedaan.
Paul Vargas
2
@PaulVargas sebenarnya di sini arshajii memposting jawaban beberapa detik sebelum ZouZou, tetapi sayangnya ZouZou menjawab pertanyaan saya sebelumnya di sini . Saya bertanya-tanya apakah saya harus menanyakan pertanyaan ini karena saya sudah tahu jawabannya tetapi sepertinya itu menarik dan ZouZou pantas mendapatkan reputasi untuk komentar sebelumnya.
Pshemo
5
Meskipun perilaku baru terlihat lebih logis , ini jelas merupakan gangguan kompatibilitas ke belakang . Satu-satunya pembenaran untuk perubahan ini "some-string".split("")adalah kasus yang sangat jarang terjadi.
ivstas
4
.split("")bukan satu-satunya cara untuk berpisah tanpa mencocokkan apa pun. Kami menggunakan regex lookahead positif yang di jdk7 yang juga cocok di awal dan menghasilkan elemen head kosong yang sekarang hilang. github.com/spray/spray/commit/…
jrudolph