¿Por qué en Java 8 split a veces se eliminan cadenas vacías al inicio de la matriz de resultados?

Antes de Java 8 cuando nos separamos en una cadena vacía como

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

el mecanismo de división se dividiría en lugares marcados con |

 |a|b|c| 

porque el espacio vacío "" existe antes y después de cada personaje. Entonces, como resultado, generaría al principio esta matriz

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

y luego eliminará las cadenas vacías finales (porque no proporcionamos explícitamente un valor negativo para limit argumento) por lo que finalmente regresará

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

En Java 8, el mecanismo de división parece haber cambiado. Ahora cuando usamos

 "abc".split("") 

obtendremos ["a", "b", "c"] matriz ["a", "b", "c"] lugar de ["", "a", "b", "c"] para que parezca que las cadenas vacías al inicio también se eliminarán. Pero esta teoría falla porque, por ejemplo

 "abc".split("a") 

está devolviendo una matriz con una cadena vacía al inicio ["", "bc"] .

¿Puede alguien explicar qué está pasando aquí y cómo han cambiado las reglas de división para estos casos en Java 8?

El comportamiento de String.split (que llama a Pattern.split ) cambia entre Java 7 y Java 8.

Documentación

Comparando la documentación de Pattern.split en Java 7 y Java 8 , observamos que se agrega la siguiente cláusula:

Cuando hay una coincidencia de ancho positivo al comienzo de la secuencia de entrada, se incluye una subcadena principal vacía al comienzo de la matriz resultante. Sin embargo, una coincidencia de ancho cero al principio nunca produce dicha subcadena principal vacía.

La misma cláusula también se agrega a String.split en Java 8 , en comparación con Java 7 .

Implementación de referencia

Comparemos el código de Pattern.split de la Pattern.split de referencia en Java 7 y Java 8. El código se obtiene de grepcode, para la versión 7u40-b43 y 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); } 

La adición del siguiente código en Java 8 excluye la coincidencia de longitud cero al comienzo de la cadena de entrada, lo que explica el comportamiento anterior.

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

Mantenimiento de compatibilidad

Siguiente comportamiento en Java 8 y superior

Para hacer que split comporte de manera consistente en todas las versiones y sea compatible con el comportamiento en Java 8:

  1. Si su expresión regular puede coincidir con cadena de longitud cero, solo agregue (?!\A) al final de la expresión regular y ajuste la expresión regular original en el grupo que no captura (?:...) (si es necesario).
  2. Si su expresión regular no puede coincidir con la cadena de longitud cero, no necesita hacer nada.
  3. Si no sabe si la expresión regular puede coincidir con la cadena de longitud cero o no, realice ambas acciones en el paso 1.

(?!\A) comprueba que la cadena no termina al principio de la cadena, lo que implica que la coincidencia es una coincidencia vacía al comienzo de la cadena.

Siguiendo el comportamiento en Java 7 y anteriores

No existe una solución general para hacer que la split compatible con versiones anteriores de Java 7 y anteriores, excepto la sustitución de todas las instancias de split por punto en su propia implementación personalizada.

Esto ha sido especificado en la documentación de split(String regex, limit) .

Cuando hay una coincidencia de ancho positivo al comienzo de esta cadena, se incluye una subcadena principal vacía al comienzo de la matriz resultante. Sin embargo, una coincidencia de ancho cero al principio nunca produce dicha subcadena principal vacía.

En "abc".split("") tiene una coincidencia de ancho cero al principio, por lo que la subcadena vacía principal no está incluida en la matriz resultante.

Sin embargo, en su segundo fragmento cuando se divide en "a" , obtuvo una coincidencia de ancho positivo (1 en este caso), por lo que la subcadena principal vacía se incluye como se esperaba.

(Eliminado el código fuente irrelevante)

Hubo un ligero cambio en los documentos para split() de Java 7 a Java 8. Específicamente, se agregó la siguiente statement:

Cuando hay una coincidencia de ancho positivo al comienzo de esta cadena, se incluye una subcadena principal vacía al comienzo de la matriz resultante. Sin embargo, una coincidencia de ancho cero al principio nunca produce dicha subcadena principal vacía.

(énfasis mío)

La división de cadena vacía genera una coincidencia de ancho cero al principio, por lo que una cadena vacía no se incluye al comienzo de la matriz resultante de acuerdo con lo especificado anteriormente. Por el contrario, su segundo ejemplo que se divide en "a" genera una coincidencia de ancho positivo al comienzo de la cadena, por lo que una cadena vacía se incluye de hecho al comienzo de la matriz resultante.