Perché in Java 8 split a volte rimuove le stringhe vuote all'inizio dell'array dei risultati?


110

Prima di Java 8, quando dividiamo su una stringa vuota come

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

il meccanismo di divisione si dividerebbe nei punti contrassegnati con |

|a|b|c|

perché lo spazio vuoto ""esiste prima e dopo ogni carattere. Quindi come risultato genererebbe inizialmente questo array

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

e successivamente rimuoverà le stringhe vuote finali (perché non abbiamo fornito esplicitamente un valore negativo limitall'argomento) in modo che alla fine tornerà

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

In Java 8 il meccanismo di divisione sembra essere cambiato. Ora quando usiamo

"abc".split("")

otterremo ["a", "b", "c"]array invece di ["", "a", "b", "c"]così sembra che anche le stringhe vuote all'inizio vengano rimosse. Ma questa teoria fallisce perché, ad esempio

"abc".split("a")

restituisce un array con una stringa vuota all'inizio ["", "bc"].

Qualcuno può spiegare cosa sta succedendo qui e come sono cambiate le regole di divisione in Java 8?


Java8 sembra risolverlo. Nel frattempo, s.split("(?!^)")sembra funzionare.
shkschneider

2
@shkschneider Il comportamento descritto nella mia domanda non è un bug delle versioni precedenti di Java-8. Questo comportamento non era particolarmente utile, ma era comunque corretto (come mostrato nella mia domanda), quindi non possiamo dire che sia stato "corretto". Lo vedo più come un miglioramento, quindi potremmo usare split("")invece di criptico (per le persone che non usano regex) split("(?!^)")o split("(?<!^)")o poche altre espressioni regolari.
Pshemo

1
Ho riscontrato lo stesso problema dopo l'aggiornamento di Fedora a Fedora 21, Fedora 21 viene fornito con JDK 1.8 e la mia applicazione di gioco IRC non funziona a causa di ciò.
LiuYan 刘 研

7
Questa domanda sembra essere l'unica documentazione di questo cambiamento radicale in Java 8. Oracle lo ha escluso dal loro elenco di incompatibilità .
Sean Van Gorder

4
Questa modifica nel JDK mi è costata solo 2 ore per rintracciare ciò che non va. Il codice funziona bene nel mio computer (JDK8) ma fallisce misteriosamente su un'altra macchina (JDK7). Oracle DOVREBBE DAVVERO aggiornare la documentazione di String.split (String regex) , piuttosto che in Pattern.split o String.split (String regex, int limit) in quanto questo è di gran lunga l'utilizzo più comune. Java è noto per la sua portabilità, ovvero il cosiddetto WORA. Si tratta di un importante cambiamento all'indietro e per nulla ben documentato.
PoweredByRice

Risposte:


84

Il comportamento di String.split(che chiama Pattern.split) cambia tra Java 7 e Java 8.

Documentazione

Confrontando tra la documentazione Pattern.splitin Java 7 e Java 8 , osserviamo la seguente clausola che si aggiunge:

Quando c'è una corrispondenza di larghezza positiva all'inizio della sequenza di input, viene inclusa una sottostringa iniziale vuota all'inizio della matrice risultante. Una corrispondenza di larghezza zero all'inizio tuttavia non produce mai una sottostringa iniziale così vuota.

La stessa clausola viene aggiunta anche String.splitin Java 8 , rispetto a Java 7 .

Implementazione di riferimento

Confrontiamo il codice dell'implementazione Pattern.splitdi riferimento in Java 7 e Java 8. Il codice viene recuperato da grepcode, per le versioni 7u40-b43 e 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);
}

L'aggiunta del seguente codice in Java 8 esclude la corrispondenza di lunghezza zero all'inizio della stringa di input, il che spiega il comportamento precedente.

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

Mantenere la compatibilità

Seguendo il comportamento in Java 8 e versioni successive

Per fare in splitmodo che si comporti in modo coerente tra le versioni e compatibile con il comportamento in Java 8:

  1. Se la tua regex può corrispondere a una stringa di lunghezza zero, aggiungi semplicemente (?!\A)alla fine dell'espressione regolare e avvolgi la regex originale in un gruppo non di acquisizione (?:...)(se necessario).
  2. Se la tua regex non può corrispondere a una stringa di lunghezza zero, non devi fare nulla.
  3. Se non sai se la regex può abbinare o meno una stringa di lunghezza zero, esegui entrambe le azioni nel passaggio 1.

(?!\A) controlla che la stringa non finisca all'inizio della stringa, il che implica che la corrispondenza è una corrispondenza vuota all'inizio della stringa.

Seguendo il comportamento in Java 7 e precedenti

Non esiste una soluzione generale per rendere splitretrocompatibile con Java 7 e precedenti, a meno di sostituire tutte le istanze di splitper puntare alla propria implementazione personalizzata.


Qualche idea su come posso modificare il split("")codice in modo che sia coerente tra le diverse versioni di Java?
Daniel

2
@Daniel: è possibile renderlo compatibile con le versioni successive (seguire il comportamento di Java 8) aggiungendo (?!^)alla fine della regex e avvolgere la regex originale in un gruppo di non acquisizione (?:...)(se necessario), ma non riesco a pensare a nessuno modo per renderlo compatibile con le versioni precedenti (seguire il vecchio comportamento in Java 7 e precedenti).
nhahtdh

Grazie per la spiegazione. Potresti descrivere "(?!^)"? In quali scenari sarà diverso ""? (Sono terribile con le espressioni regolari!: - /).
Daniel

1
@Daniel: il suo significato è influenzato dalla Pattern.MULTILINEbandiera, mentre \Acorrisponde sempre all'inizio della stringa indipendentemente dalle bandiere.
nhahtdh

30

Questo è stato specificato nella documentazione di split(String regex, limit).

Quando c'è una corrispondenza di larghezza positiva all'inizio di questa stringa, viene inclusa una sottostringa iniziale vuota all'inizio della matrice risultante. Una corrispondenza di larghezza zero all'inizio tuttavia non produce mai una sottostringa iniziale così vuota.

In "abc".split("")hai una corrispondenza di larghezza zero all'inizio, quindi la sottostringa vuota iniziale non è inclusa nell'array risultante.

Tuttavia, nel secondo snippet, quando ci si divide, "a"si ottiene una corrispondenza di larghezza positiva (1 in questo caso), quindi la sottostringa iniziale vuota è inclusa come previsto.

(Rimosso codice sorgente irrilevante)


3
È solo una domanda. Va bene inserire un frammento di codice dal JDK? Ricordi il problema del copyright con Google - Harry Potter - Oracle?
Paul Vargas

6
@PaulVargas Per essere onesti, non lo so, ma presumo sia ok dato che puoi scaricare il JDK e decomprimere il file src che contiene tutti i sorgenti. Quindi tecnicamente tutti potrebbero vedere la fonte.
Alexis C.

12
@PaulVargas "open" in "open source" significa qualcosa.
Marko Topolnik

2
@ZouZou: solo perché tutti possono vederlo non significa che tu possa ripubblicarlo
user102008

2
@Paul Vargas, IANAL ma in molte altre occasioni questo tipo di post rientra nella situazione del preventivo / fair use. Maggiori informazioni sull'argomento sono qui: meta.stackexchange.com/questions/12527/…
Alex Pakka

14

C'è stata una leggera modifica nei documenti per split()da Java 7 a Java 8. In particolare, è stata aggiunta la seguente dichiarazione:

Quando c'è una corrispondenza di larghezza positiva all'inizio di questa stringa, viene inclusa una sottostringa iniziale vuota all'inizio della matrice risultante. Una corrispondenza di larghezza zero all'inizio tuttavia non produce mai una sottostringa iniziale così vuota.

(enfasi mia)

La divisione della stringa vuota genera una corrispondenza di larghezza zero all'inizio, quindi una stringa vuota non viene inclusa all'inizio della matrice risultante in conformità con quanto specificato sopra. Al contrario, il tuo secondo esempio che si divide su "a"genera una corrispondenza di larghezza positiva all'inizio della stringa, quindi una stringa vuota viene di fatto inclusa all'inizio della matrice risultante.


Qualche secondo in più ha fatto la differenza.
Paul Vargas

2
@PaulVargas in realtà qui arshajii ha pubblicato la risposta pochi secondi prima di ZouZou, ma sfortunatamente ZouZou ha risposto alla mia domanda qui sopra . Mi chiedevo se avrei dovuto porre questa domanda dato che conoscevo già una risposta, ma sembrava interessante e ZouZou meritava una certa reputazione per il suo commento precedente.
Pshemo

5
Nonostante il nuovo comportamento sembri più logico , è ovviamente una rottura della compatibilità con le versioni precedenti . L'unica giustificazione per questo cambiamento è che "some-string".split("")è un caso piuttosto raro.
ivstas

4
.split("")non è l'unico modo per dividere senza abbinare nulla. Abbiamo usato una regex lookahead positiva che in jdk7 corrispondeva anche all'inizio e produceva un elemento head vuoto che ora non c'è più. github.com/spray/spray/commit/…
jrudolph
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.