De ce în Java 8 split elimină uneori șiruri goale la începutul tabloului de rezultate? (Programare, Java, Regex, Split, Java 8)

Pshemo a intrebat.
a intrebat.

Înainte de Java 8 atunci când împărțim pe un șir de caractere gol, cum ar fi

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

mecanismul de divizare ar diviza în locurile marcate cu |

|a|b|c|

deoarece spațiul gol "" există înainte și după fiecare caracter. Prin urmare, la început se genera această matrice

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

iar mai târziu va elimina șirurile goale din urmă (pentru că nu am furnizat în mod explicit o valoare negativă pentru limit argument), astfel încât în final va returna

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

În Java 8 mecanismul de divizare pare să se fi schimbat. Acum, când folosim

"abc".split("")

vom obține ["a", "b", "c"] array în loc de ["", "a", "b", "c"] astfel încât se pare că șirurile goale de la început sunt, de asemenea, eliminate. Dar această teorie eșuează deoarece, de exemplu

"abc".split("a")

returnează o matrice cu un șir gol la început ["", "bc"].

Poate cineva să explice ce se întâmplă aici și cum s-au schimbat regulile de divizare în Java 8?

Comentarii

  • Java8 pare să rezolve această problemă. Între timp, s.split("(?!^)") pare să funcționeze. –  > Por shkschneider.
  • @shkschneider Comportamentul descris în întrebarea mea nu este o eroare a versiunilor anterioare lui Java-8. Acest comportament nu era deosebit de util, dar era totuși corect (așa cum se arată în întrebarea mea), așa că nu putem spune că a fost „reparat”. Eu o văd mai degrabă ca pe o îmbunătățire, astfel încât să putem folosi split("") în loc de criptic (pentru persoanele care nu folosesc regex) split("(?!^)") sau split("(?<!^)") sau alte câteva regexuri. –  > Por Pshemo.
  • Am întâlnit aceeași problemă după ce am actualizat fedora la Fedora 21, fedora 21 vine cu JDK 1.8, iar aplicația mea de joc IRC este stricată din această cauză. –  > Por LiuYan 刘研.
  • Această întrebare pare să fie singura documentație a acestei schimbări de rupere în Java 8. Oracle a omis să o includă în lista de incompatibilități. –  > Por Sean Van Gorder.
  • Această schimbare în JDK tocmai m-a costat 2 ore de urmărire a ceea ce este greșit. Codul rulează bine pe calculatorul meu (JDK8), dar eșuează în mod misterios pe o altă mașină (JDK7). Oracle CHIAR AR TREBUI SĂ să actualizeze documentația pentru String.split(String regex), , mai degrabă decât în Pattern.split sau String.split(String regex, int limit), deoarece aceasta este de departe cea mai frecventă utilizare. Java este cunoscut pentru portabilitatea sa, cunoscută și sub numele de WORA. Aceasta este o schimbare majoră care rupe cu mult timp în urmă și nu este deloc bine documentată. –  > Por PoweredByRice.
3 răspunsuri
nhahtdh

Comportamentul de String.split (care apelează Pattern.split) se schimbă între Java 7 și Java 8.

Documentație

Comparând documentația pentru Pattern.split în Java 7 și Java 8, , observăm că a fost adăugată următoarea clauză:

În cazul în care există o potrivire de lățime pozitivă la începutul secvenței de intrare, atunci o subșiră de început goală este inclusă la începutul matricei rezultate. Cu toate acestea, o potrivire de lățime zero la început nu produce niciodată o astfel de subșir de început gol.

Aceeași clauză se adaugă și la String.split în Java 8, , în comparație cu Java 7.

Implementarea de referință

Să comparăm codul de Pattern.split implementării de referință în Java 7 și Java 8. Codul este preluat din grepcode, pentru versiunile 7u40-b43 și 8-b132.

Java 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);
}

Java 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);
}

Adăugarea următorului cod în Java 8 exclude potrivirea de lungime zero la începutul șirului de intrare, ceea ce explică comportamentul de mai sus.

            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;
            }

Menținerea compatibilității

Următorul comportament în Java 8 și versiunile superioare

Pentru a face split se comportă în mod consecvent în toate versiunile și să fie compatibil cu comportamentul din Java 8:

  1. Dacă regex-ul dvs. poate se potrivește cu un șir de lungime zero, trebuie doar să adăugați (?!A) la la sfârșit al regex-ului și să înfășurați regex-ul original într-un grup care nu capturează (?:...) (dacă este necesar).
  2. În cazul în care regex-ul dvs. nu poate se potrivește cu un șir de lungime zero, nu trebuie să faceți nimic.
  3. Dacă nu știți dacă regex-ul poate sau nu să se potrivească cu șirul de lungime zero, efectuați ambele acțiuni de la pasul 1.

(?!A) verifică dacă șirul nu se termină la începutul șirului, ceea ce implică faptul că potrivirea este o potrivire goală la începutul șirului.

Următorul comportament în Java 7 și în versiunile anterioare

Nu există o soluție generală pentru a face ca split compatibilă cu Java 7 și cu versiunile anterioare, fără a înlocui toate instanțele de split pentru a indica propria implementare personalizată.

Comentarii

  • Aveți vreo idee despre cum pot schimba split("") codul astfel încât să fie consecvent între diferitele versiuni Java? –  > Por Daniel.
  • @Daniel: Este posibil să îl faceți compatibil cu viitor (să urmați comportamentul Java 8) adăugând (?!^) la la sfârșit al regex-ului și înfășurarea regex-ului original într-un grup care nu capturează (?:...) (dacă este necesar), dar nu mă pot gândi la nicio modalitate de a-l face compatibil cu trecutul (să urmeze vechiul comportament din Java 7 și anterioare). –  > Por nhahtdh.
  • Mulțumesc pentru explicații. Ați putea descrie "(?!^)"? În ce scenarii va fi diferit de ""? (Sunt groaznic la regex! :-/). –  > Por Daniel.
  • @Daniel: Semnificația sa este afectată de Pattern.MULTILINE flag, în timp ce A se potrivește întotdeauna la începutul șirului, indiferent de steaguri. –  > Por nhahtdh.
Alexis C.

Acest lucru a fost specificat în documentația lui split(String regex, limit).

Atunci când există o potrivire de lățime pozitivă la începutul acestui șir de caractere, se include o subșiră de început goală la începutul matricei rezultate. Cu toate acestea, o potrivire de lățime zero la început nu produce niciodată o astfel de subșir de început gol.

În "abc".split("") s-a obținut o potrivire de lățime zero la început, astfel încât subșirul de început gol nu este inclus în matricea rezultată.

Cu toate acestea, în cel de-al doilea fragment al dvs., atunci când împărțiți pe "a" ați obținut o potrivire cu o lățime pozitivă (1 în acest caz), astfel încât substringa goală din față este inclusă, așa cum era de așteptat.

(A fost eliminat codul sursă irelevant)

Comentarii

  • Este doar o întrebare. Este în regulă să postați un fragment de cod din JDK? Vă amintiți de problema drepturilor de autor cu Google – Harry Potter – Oracle? –  > Por Paul Vargas.
  • @PaulVargas Pentru a fi corect, nu știu, dar presupun că este în regulă, deoarece puteți descărca JDK și descompuneți fișierul src care conține toate sursele. Deci, din punct de vedere tehnic, toată lumea ar putea vedea sursa. –  > Por Alexis C..
  • @PaulVargas „open” din „open source” înseamnă ceva. –  > Por Marko Topolnik.
  • @ZouZou: doar pentru că toată lumea poate vedea nu înseamnă că poți să o re-publici – -.  > Por user102008.
  • @Paul Vargas, IANAL, dar în multe alte ocazii acest tip de post se încadrează în situația de citare / fair use. Mai multe pe această temă găsiți aici: meta.stackexchange.com/questions/12527/… -…  > Por Alex Pakka.
arshajii

A existat o mică modificare în documentele pentru split() de la Java 7 la Java 8. Mai exact, a fost adăugată următoarea declarație:

Atunci când există o potrivire de lățime pozitivă la începutul acestui șir, atunci o subșiră de început goală este inclusă la începutul matricei rezultate. Cu toate acestea, o potrivire de lățime zero la început nu produce niciodată o astfel de subșir de început gol.

(sublinierea îmi aparține)

Divizarea șirului gol generează o potrivire de lățime zero la început, astfel încât un șir gol nu este inclus la începutul matricei rezultate, în conformitate cu cele specificate mai sus. În schimb, cel de-al doilea exemplu al dvs. care împarte pe "a" generează un pozitiv-la începutul șirului, astfel încât un șir gol este de fapt inclus la începutul tabloului rezultat.

Comentarii

  • Câteva secunde în plus au făcut diferența. –  > Por Paul Vargas.
  • @PaulVargas de fapt aici arshajii a postat răspunsul cu câteva secunde înainte de ZouZou, dar din păcate ZouZou mi-a răspuns mai devreme la întrebarea mea aici. Mă întrebam dacă ar trebui să pun această întrebare, deoarece știam deja un răspuns, dar părea una interesantă și ZouZou merita o anumită reputație pentru comentariul său anterior. –  > Por Pshemo.
  • În ciuda faptului că noul comportament arată mai mult logic, , este în mod evident un o întrerupere a compatibilității cu trecutul. Singura justificare pentru această modificare este că "some-string".split("") este un caz destul de rar. –  > Por ivstas.
  • .split("") nu este singura modalitate de a împărți fără a se potrivi nimic. Am folosit un regex pozitiv lookahead care în jdk7 care, de asemenea, se potrivea la început și producea un element de cap gol, care acum nu mai există. github.com/spray/spray/commit/… –  > Por jrudolph.