Dlaczego w Java 8 split czasami usuwa puste ciągi na początku tablicy wynikowej?

110

Przed Java 8, kiedy dzielimy się na pusty ciąg, taki jak

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

mechanizm split rozszczepiłby się w miejscach oznaczonych |

|a|b|c|

ponieważ pusta przestrzeń ""istnieje przed i po każdym znaku. W rezultacie wygeneruje najpierw tę tablicę

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

a później usunie końcowe puste ciągi (ponieważ nie podaliśmy jawnie wartości ujemnej dla limitargumentu), więc w końcu zwróci

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

Wydaje się, że mechanizm podziału Java 8 uległ zmianie. Teraz, kiedy używamy

"abc".split("")

["a", "b", "c"]zamiast tego otrzymamy tablicę, ["", "a", "b", "c"]więc wygląda na to, że puste ciągi znaków na początku są również usuwane. Ale ta teoria zawodzi, bo na przykład

"abc".split("a")

zwraca tablicę z pustym ciągiem na początku ["", "bc"].

Czy ktoś może wyjaśnić, co się tutaj dzieje i jak zmieniły się zasady podziału w Javie 8?

Pshemo
źródło
Wydaje się, że Java8 to naprawia. Tymczasem s.split("(?!^)")wydaje się działać.
shkschneider
2
@shkschneider Zachowanie opisane w moim pytaniu nie jest błędem wcześniejszych wersji Java-8. To zachowanie nie było szczególnie przydatne, ale nadal było poprawne (jak pokazano w moim pytaniu), więc nie możemy powiedzieć, że zostało „naprawione”. Widzę to raczej poprawy, więc możemy użyć split("")zamiast tajemnicze (dla osób, które nie korzystają z regex) split("(?!^)")lub split("(?<!^)")lub kilku innych regexes.
Pshemo
1
Napotkano ten sam problem po uaktualnieniu Fedory do Fedory 21, Fedora 21 jest dostarczana z JDK 1.8, przez co moja aplikacja IRC jest zepsuta.
LiuYan 刘 研
7
Wydaje się, że to pytanie jest jedyną dokumentacją dotyczącą tej przełomowej zmiany w Javie 8. Firma Oracle pominęła ją na swojej liście niezgodności .
Sean Van Gorder
4
Ta zmiana w JDK kosztowała mnie tylko 2 godziny śledzenia, co jest nie tak. Kod działa dobrze na moim komputerze (JDK8), ale w tajemniczy sposób zawodzi na innym komputerze (JDK7). Oracle NAPRAWDĘ POWINIEN zaktualizować dokumentację String.split (String regex) zamiast Pattern.split lub String.split (String regex, int limit), ponieważ jest to zdecydowanie najczęstsze użycie. Java jest znana ze swojej przenośności, czyli tzw. WORA. Jest to poważna zmiana wsteczna i wcale niezbyt dobrze udokumentowana.
PoweredByRice

Odpowiedzi:

84

Zachowanie String.split(które wywołuje Pattern.split) zmienia się między Java 7 i Java 8.

Dokumentacja

Porównując dokumentację Pattern.splitw językach Java 7 i Java 8 , zauważamy dodanie następującej klauzuli:

Jeśli na początku sekwencji wejściowej występuje dopasowanie o dodatniej szerokości, na początku otrzymanej tablicy zostanie umieszczony pusty podciąg wiodący. Jednak dopasowanie o zerowej szerokości na początku nigdy nie tworzy takiego pustego wiodącego podciągu.

Ta sama klauzula została również dodana String.splitw Javie 8 w porównaniu z Javą 7 .

Realizacja referencyjna

Porównajmy kod Pattern.splitimplementacji referencyjnej w Javie 7 i Java 8. Kod jest pobierany z grepcode dla wersji 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);
}

Dodanie następującego kodu w Javie 8 wyklucza dopasowanie o zerowej długości na początku ciągu wejściowego, co wyjaśnia powyższe zachowanie.

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

Utrzymanie zgodności

Następujące zachowanie w Javie 8 i nowszych

Aby zapewnić splitspójne zachowanie w różnych wersjach i zgodność z zachowaniem w języku Java 8:

  1. Jeśli twoje wyrażenie regularne może pasować do łańcucha o zerowej długości, po prostu dodaj (?!\A)na końcu wyrażenia regularnego i zawiń oryginalne wyrażenie regularne w grupę nieprzechwytywaną (?:...)(jeśli to konieczne).
  2. Jeśli twoje wyrażenie regularne nie może dopasować ciągu o zerowej długości, nie musisz nic robić.
  3. Jeśli nie wiesz, czy wyrażenie regularne może pasować do ciągu o zerowej długości, czy nie, wykonaj obie czynności w kroku 1.

(?!\A) sprawdza, czy ciąg nie kończy się na początku ciągu, co oznacza, że ​​dopasowanie jest pustym dopasowaniem na początku ciągu.

Następujące zachowanie w Javie 7 i wcześniejszych

Nie ma ogólnego rozwiązania zapewniającego splitzgodność wsteczną z wersją Java 7 i wcześniejszą, poza zastąpieniem wszystkich wystąpień, splitaby wskazywały na własną niestandardową implementację.

nhahtdh
źródło
Masz pomysł, jak mogę zmienić split("")kod, aby był spójny w różnych wersjach Java?
Daniel
2
@Daniel: Jest możliwe, aby uczynić go naprzód kompatybilne (śledzić zachowanie Java 8), dodając (?!^)do końca tego regex i zawinąć oryginalnego regex w niewyspecjalizowanych przechwytywanie grupy (?:...)(jeśli to konieczne), ale nie mogę myśleć o każdym sposób, aby uczynić go kompatybilnym wstecz (postępuj zgodnie ze starym zachowaniem w Javie 7 i wcześniejszych).
nhahtdh
Dziękuję za wyjaśnienie. Czy mógłbyś opisać "(?!^)"? W jakich scenariuszach będzie się to różnić ""? (Jestem okropny w regex!: - /).
Daniel
1
@Daniel: Na jego znaczenie ma wpływ Pattern.MULTILINEflaga, podczas gdy \Azawsze pasuje na początku ciągu, niezależnie od flag.
nhahtdh
30

Zostało to określone w dokumentacji split(String regex, limit).

Jeśli na początku tego ciągu występuje dopasowanie o dodatniej szerokości, na początku wynikowej tablicy zostanie umieszczony pusty podciąg wiodący. Jednak dopasowanie o zerowej szerokości na początku nigdy nie tworzy takiego pustego wiodącego podciągu.

W "abc".split("")masz na początku dopasowanie o zerowej szerokości, więc wiodący pusty podciąg nie jest zawarty w wynikowej tablicy.

Jednak w drugim fragmencie po podzieleniu "a"otrzymałeś dodatnie dopasowanie szerokości (w tym przypadku 1), więc pusty wiodący podciąg jest uwzględniony zgodnie z oczekiwaniami.

(Usunięto nieistotny kod źródłowy)

Alexis C.
źródło
3
To jest po prostu pytanie. Czy mogę opublikować fragment kodu z JDK? Pamiętasz problem praw autorskich w Google - Harry Potter - Oracle?
Paul Vargas
6
@PaulVargas Szczerze mówiąc, nie wiem, ale zakładam, że wszystko jest w porządku, ponieważ możesz pobrać JDK i rozpakować plik src, który zawiera wszystkie źródła. Więc technicznie każdy mógł zobaczyć źródło.
Alexis C.
12
@PaulVargas Słowo „open” w „open source” oznacza coś.
Marko Topolnik
2
@ZouZou: tylko dlatego, że wszyscy to widzą, nie oznacza, że ​​możesz go ponownie opublikować
user102008
2
@Paul Vargas, IANAL, ale w wielu innych przypadkach tego typu post jest objęty cytatem / sytuacją dozwolonego użytku. Więcej na ten temat tutaj: meta.stackexchange.com/questions/12527/…
Alex Pakka
14

Nastąpiła niewielka zmiana w dokumentacji split()z języka Java 7 do języka Java 8. W szczególności dodano następujące oświadczenie:

Jeśli na początku tego ciągu występuje dopasowanie o dodatniej szerokości, na początku wynikowej tablicy zostanie umieszczony pusty podciąg wiodący. Jednak dopasowanie o zerowej szerokości na początku nigdy nie tworzy takiego pustego wiodącego podciągu.

(podkreślenie moje)

Podział na pusty ciąg generuje dopasowanie o zerowej szerokości na początku, więc pusty ciąg nie jest dołączany na początku wynikowej tablicy, zgodnie z tym, co określono powyżej. W przeciwieństwie do tego, twój drugi przykład, który dzieli się na, "a"generuje dopasowanie dodatniej szerokości na początku ciągu, więc pusty ciąg jest w rzeczywistości dołączany na początku wynikowej tablicy.

arshajii
źródło
Jeszcze kilka sekund zrobiło różnicę.
Paul Vargas
2
@PaulVargas faktycznie tutaj arshajii opublikował odpowiedź kilka sekund przed ZouZou, ale niestety ZouZou odpowiedział na moje pytanie wcześniej tutaj . Zastanawiałem się, czy powinienem zadać to pytanie, ponieważ znałem już odpowiedź, ale wydawała się interesująca, a ZouZou zasługiwał na pewną reputację za swój wcześniejszy komentarz.
Pshemo
5
Pomimo tego, że nowe zachowanie wygląda bardziej logicznie , jest to oczywiście przerwa w kompatybilności wstecznej . Jedynym uzasadnieniem tej zmiany jest to, że "some-string".split("")jest to dość rzadki przypadek.
ivstas
4
.split("")to nie jedyny sposób na dzielenie bez dopasowywania czegokolwiek. Użyliśmy pozytywnego wyrażenia regularnego lookahead, które w jdk7 również pasowało na początku i tworzyło pusty element head, którego teraz nie ma. github.com/spray/spray/commit/…
jrudolph