Warum entfernt Split in Java 8 manchmal leere Zeichenfolgen am Anfang des Ergebnisarrays?

110

Vor Java 8 teilen wir uns auf leere Zeichenfolgen wie

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

Der Aufteilungsmechanismus würde sich an Stellen aufteilen, die mit markiert sind |

|a|b|c|

weil ""vor und nach jedem Zeichen ein leerer Raum existiert. Als Ergebnis würde es zunächst dieses Array erzeugen

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

und später werden nachfolgende leere Zeichenfolgen entfernt (da wir dem limitArgument keinen expliziten negativen Wert gegeben haben ), sodass es schließlich zurückkehrt

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

In Java 8 scheint sich der Split-Mechanismus geändert zu haben. Nun, wenn wir verwenden

"abc".split("")

Wir werden ["a", "b", "c"]stattdessen ein Array erhalten, ["", "a", "b", "c"]so dass es so aussieht, als würden leere Zeichenfolgen beim Start ebenfalls entfernt. Aber diese Theorie scheitert zum Beispiel daran

"abc".split("a")

Gibt beim Start ein Array mit einer leeren Zeichenfolge zurück ["", "bc"].

Kann jemand erklären, was hier vor sich geht und wie sich die Split-Regeln in Java 8 geändert haben?

Pshemo
quelle
Java8 scheint das zu beheben. Inzwischen s.split("(?!^)")scheint zu funktionieren.
Shkschneider
2
@shkschneider Das in meiner Frage beschriebene Verhalten ist kein Fehler von Versionen vor Java-8. Dieses Verhalten war nicht besonders nützlich, aber es war immer noch korrekt (wie in meiner Frage gezeigt), sodass wir nicht sagen können, dass es "behoben" wurde. Ich sehe es eher wie Verbesserung , so dass wir nutzen könnten , split("")anstatt kryptischer (für Leute , die benutzen regex nicht) split("(?!^)")oder split("(?<!^)")oder einige andere reguläre Ausdrücke.
Pshemo
1
Nach dem Upgrade von Fedora auf Fedora 21 ist Fedora 21 mit JDK 1.8 ausgeliefert worden, und meine IRC-Spieleanwendung ist aus diesem Grund fehlerhaft.
LiuYan
7
Diese Frage scheint die einzige Dokumentation für diese grundlegende Änderung in Java 8 zu sein. Oracle hat sie aus der Liste der Inkompatibilitäten gestrichen .
Sean Van Gorder
4
Diese Änderung im JDK hat mich nur 2 Stunden gekostet, um herauszufinden, was falsch ist. Der Code läuft auf meinem Computer (JDK8) einwandfrei, schlägt jedoch auf einem anderen Computer (JDK7) auf mysteriöse Weise fehl. Oracle sollte wirklich die Dokumentation aktualisieren String.split (String regex) , anstatt in Pattern.split oder String.split (String regex, int limit) , da dies bei weitem der häufigste Verwendung ist. Java ist bekannt für seine Portabilität, auch bekannt als WORA. Dies ist eine große rückwärtsbrechende Änderung und überhaupt nicht gut dokumentiert.
PoweredByRice

Antworten:

84

Das Verhalten von String.split(welches aufruft Pattern.split) ändert sich zwischen Java 7 und Java 8.

Dokumentation

Beim Vergleich zwischen der Dokumentation Pattern.splitin Java 7 und Java 8 wird die folgende Klausel hinzugefügt:

Wenn zu Beginn der Eingabesequenz eine Übereinstimmung mit positiver Breite vorliegt, wird am Anfang des resultierenden Arrays eine leere führende Teilzeichenfolge eingefügt. Eine Übereinstimmung mit der Breite Null am Anfang erzeugt jedoch niemals einen solchen leeren führenden Teilstring.

Dieselbe Klausel wird auch String.splitin Java 8 im Vergleich zu Java 7 hinzugefügt .

Referenzimplementierung

Vergleichen wir den Code Pattern.splitder Referenzimplemetation in Java 7 und Java 8. Der Code wird aus grepcode für die Versionen 7u40-b43 und 8-b132 abgerufen.

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

Das Hinzufügen des folgenden Codes in Java 8 schließt die Übereinstimmung mit der Länge Null am Anfang der Eingabezeichenfolge aus, was das obige Verhalten erklärt.

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

Kompatibilität aufrechterhalten

Folgendes Verhalten in Java 8 und höher

So splitverhalten Sie sich über Versionen hinweg konsistent und mit dem Verhalten in Java 8 kompatibel:

  1. Wenn Ihre Regex mit einer Zeichenfolge mit der Länge Null übereinstimmen kann , fügen Sie sie einfach (?!\A)am Ende der Regex hinzu und verpacken Sie die ursprüngliche Regex in eine nicht erfassende Gruppe (?:...)(falls erforderlich).
  2. Wenn Ihre Regex nicht mit einer Zeichenfolge mit der Länge Null übereinstimmen kann, müssen Sie nichts tun.
  3. Wenn Sie nicht wissen, ob der reguläre Ausdruck mit einer Zeichenfolge mit der Länge Null übereinstimmen kann, führen Sie beide Aktionen in Schritt 1 aus.

(?!\A) Überprüft, ob die Zeichenfolge nicht am Anfang der Zeichenfolge endet. Dies bedeutet, dass die Übereinstimmung am Anfang der Zeichenfolge eine leere Übereinstimmung ist.

Folgendes Verhalten in Java 7 und früher

Es gibt keine allgemeine Lösung, um die splitAbwärtskompatibilität mit Java 7 und früheren Versionen zu gewährleisten, ohne alle Instanzen von splitzu ersetzen , um auf Ihre eigene benutzerdefinierte Implementierung zu verweisen.

nhahtdh
quelle
Haben Sie eine Idee, wie ich split("")Code so ändern kann, dass er über verschiedene Java-Versionen hinweg konsistent ist?
Daniel
2
@ Daniel: Es ist möglich, es vorwärtskompatibel zu machen (folgen Sie dem Verhalten von Java 8), indem Sie (?!^)es am Ende des regulären Ausdrucks hinzufügen und den ursprünglichen regulären Ausdruck in eine nicht erfassende Gruppe einschließen (?:...)(falls erforderlich), aber ich kann mir keine vorstellen Möglichkeit, es abwärtskompatibel zu machen (folgen Sie dem alten Verhalten in Java 7 und früheren Versionen).
nhahtdh
Danke für die Erklärung. Könnten Sie beschreiben "(?!^)"? In welchen Szenarien wird es anders sein ""? (Ich bin schrecklich bei Regex !: - /).
Daniel
1
@ Daniel: Seine Bedeutung wird durch das Pattern.MULTILINEFlag beeinflusst, während es \Aunabhängig von den Flags immer am Anfang der Zeichenfolge übereinstimmt.
nhahtdh
30

Dies wurde in der Dokumentation von angegeben split(String regex, limit).

Wenn am Anfang dieser Zeichenfolge eine Übereinstimmung mit positiver Breite vorliegt, wird am Anfang des resultierenden Arrays eine leere führende Teilzeichenfolge eingefügt. Eine Übereinstimmung mit der Breite Null am Anfang erzeugt jedoch niemals einen solchen leeren führenden Teilstring.

In haben "abc".split("")Sie am Anfang eine Übereinstimmung mit der Breite Null erhalten, sodass der führende leere Teilstring nicht im resultierenden Array enthalten ist.

In Ihrem zweiten Snippet haben Sie jedoch beim "a"Teilen eine positive Breitenübereinstimmung erhalten (in diesem Fall 1), sodass der leere führende Teilstring wie erwartet enthalten ist.

(Irrelevanten Quellcode entfernt)

Alexis C.
quelle
3
Es ist nur eine Frage. Ist es in Ordnung, ein Codefragment aus dem JDK zu veröffentlichen? Erinnern Sie sich an das Urheberrechtsproblem mit Google - Harry Potter - Oracle?
Paul Vargas
6
@PaulVargas Um fair zu sein, ich weiß es nicht, aber ich gehe davon aus, dass es in Ordnung ist, da Sie das JDK herunterladen und die src-Datei entpacken können, die alle Quellen enthält. Technisch gesehen konnte also jeder die Quelle sehen.
Alexis C.
12
@PaulVargas Das "Öffnen" in "Open Source" steht für etwas.
Marko Topolnik
2
@ ZouZou: Nur weil jeder es sehen kann, heißt das nicht, dass Sie es erneut veröffentlichen können
user102008
2
@Paul Vargas, IANAL, aber in vielen anderen Fällen fällt diese Art von Post unter die Quote / Fair-Use-Situation. Mehr zum Thema finden Sie hier: meta.stackexchange.com/questions/12527/…
Alex Pakka
14

In den Dokumenten für split()Java 7 wurde eine geringfügige Änderung von Java 8 vorgenommen. Insbesondere wurde die folgende Anweisung hinzugefügt:

Wenn am Anfang dieser Zeichenfolge eine Übereinstimmung mit positiver Breite vorliegt, wird am Anfang des resultierenden Arrays eine leere führende Teilzeichenfolge eingefügt. Eine Übereinstimmung mit der Breite Null am Anfang erzeugt jedoch niemals einen solchen leeren führenden Teilstring.

(Hervorhebung von mir)

Die Aufteilung der leeren Zeichenfolge generiert zu Beginn eine Übereinstimmung mit der Breite Null, sodass am Anfang des resultierenden Arrays keine leere Zeichenfolge gemäß den obigen Angaben enthalten ist. Im Gegensatz dazu "a"generiert Ihr zweites Beispiel, das sich aufteilt, am Anfang der Zeichenfolge eine positive Breitenübereinstimmung, sodass am Anfang des resultierenden Arrays tatsächlich eine leere Zeichenfolge enthalten ist.

arshajii
quelle
Noch ein paar Sekunden machten den Unterschied.
Paul Vargas
2
@PaulVargas eigentlich hier arshajii hat einige Sekunden vor ZouZou eine Antwort gepostet, aber leider hat ZouZou meine Frage hier früher beantwortet . Ich fragte mich, ob ich diese Frage stellen sollte, da ich bereits eine Antwort wusste, aber sie schien interessant zu sein, und ZouZou verdiente einen gewissen Ruf für seinen früheren Kommentar.
Pshemo
5
Obwohl das neue Verhalten logischer aussieht , handelt es sich offensichtlich um eine Abwärtskompatibilitätsunterbrechung . Die einzige Rechtfertigung für diese Änderung ist, dass dies "some-string".split("")ein ziemlich seltener Fall ist.
ivstas
4
.split("")ist nicht die einzige Möglichkeit, sich zu teilen, ohne etwas zusammenzubringen. Wir haben einen positiven Lookahead-Regex verwendet, der in jdk7 ebenfalls am Anfang übereinstimmte und ein leeres Kopfelement erzeugte, das jetzt weg ist. github.com/spray/spray/commit/…
jrudolph