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

Prima di Java 8 quando ci siamo divisi su una stringa vuota come

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

il meccanismo di divisione si dividerebbe in punti contrassegnati da |

 |a|b|c| 

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

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

e in seguito rimuoverà le stringhe vuote finali (perché non abbiamo fornito un valore negativo per limit argomento) in modo che alla fine ritorni

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

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

 "abc".split("") 

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

 "abc".split("a") 

sta restituendo un array con una stringa vuota all’inizio ["", "bc"] .

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

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

Documentazione

Confrontando la documentazione di Pattern.split in Java 7 e Java 8 , osserviamo la seguente clausola aggiunta:

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

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

Implementazione di riferimento

Confrontiamo il codice di Pattern.split dell’implemetazione di riferimento in Java 7 e Java 8. Il codice viene recuperato da grepcode, per la versione 7u40-b43 e 8-b132.

Java 7

 public String[] split(CharSequence input, int limit) { int index = 0; boolean matchLimited = limit > 0; ArrayList 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 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, 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; } 

Mantenimento della compatibilità

Seguente comportamento in Java 8 e versioni successive

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

  1. Se la tua espressione regolare può corrispondere a una stringa di lunghezza zero, basta aggiungere (?!\A) alla fine della regex e avvolgere la regex originale nel gruppo non di acquisizione (?:...) (se necessario).
  2. Se la tua espressione regolare non può corrispondere a una stringa di lunghezza zero, non devi fare nulla.
  3. Se non sai se la regex può corrispondere o meno alla 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.

Seguente comportamento in Java 7 e precedenti

Non esiste una soluzione generale per rendere split backward-compatible con Java 7 e versioni precedenti, a meno di sostituire tutte le istanze di split per puntare alla propria implementazione personalizzata.

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

Quando all’inizio della stringa è presente una corrispondenza di larghezza positiva, all’inizio della matrice risultante viene inclusa una sottostringa principale vuota. Una corrispondenza di larghezza zero all’inizio tuttavia non produce mai una sottostringa così vuota.

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

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

(Rimosso codice sorgente irrilevante)

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

Quando all’inizio della stringa è presente una corrispondenza di larghezza positiva, all’inizio della matrice risultante viene inclusa una sottostringa principale vuota. Una corrispondenza di larghezza zero all’inizio tuttavia non produce mai una sottostringa così vuota.

(sottolineatura mia)

La divisione stringa vuota genera una corrispondenza di larghezza zero all’inizio, quindi una stringa vuota non è inclusa all’inizio dell’array risultante in base a quanto specificato sopra. Al contrario, il 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 dell’array risultante.