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

110

Antes de Java 8 cuando dividimos 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 ""existe un espacio vacío antes y después de cada carácter. 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 al limitargumento) para que finalmente regrese

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

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

"abc".split("")

obtendremos una ["a", "b", "c"]matriz en lugar de, ["", "a", "b", "c"]por lo que parece que las cadenas vacías al inicio también se eliminan. Pero esta teoría falla porque, por ejemplo,

"abc".split("a")

devuelve una matriz con una cadena vacía al inicio ["", "bc"].

¿Alguien puede explicar qué está pasando aquí y cómo han cambiado las reglas de división en Java 8?

Pshemo
fuente
Java8 parece arreglar eso. Mientras tanto, s.split("(?!^)")parece funcionar.
shkschneider
2
@shkschneider El comportamiento descrito en mi pregunta no es un error de las versiones anteriores a Java-8. Este comportamiento no fue particularmente útil, pero aún era correcto (como se muestra en mi pregunta), por lo que no podemos decir que fue "arreglado". Lo veo más como una mejora para que pudiéramos utilizar split("")en lugar de crípticos (para las personas que no usan expresiones regulares) split("(?!^)")o split("(?<!^)")o algunos otros expresiones regulares.
Pshemo
1
Encontré el mismo problema después de actualizar fedora a Fedora 21, fedora 21 viene con JDK 1.8, y mi aplicación de juego IRC está rota debido a esto.
LiuYan 刘 研
7
Esta pregunta parece ser la única documentación de este cambio radical en Java 8. Oracle lo dejó fuera de su lista de incompatibilidades .
Sean Van Gorder
4
Este cambio en el JDK solo me costó 2 horas rastrear lo que está mal. El código funciona bien en mi computadora (JDK8) pero falla misteriosamente en otra máquina (JDK7). Oracle REALMENTE DEBE actualizar la documentación de String.split (String regex) , en lugar de Pattern.split o String.split (String regex, int limit) ya que este es, con mucho, el uso más común. Java es conocido por su portabilidad, también conocido como WORA. Este es un cambio importante que rompe hacia atrás y no está bien documentado en absoluto.
PoweredByRice

Respuestas:

84

El comportamiento de String.split(qué llamadas Pattern.split) cambia entre Java 7 y Java 8.

Documentación

Comparando entre la documentación de Pattern.spliten Java 7 y Java 8 , se observa la siguiente cláusula se añade:

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 una subcadena inicial vacía.

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

Implementación de referencia

Comparemos el código de Pattern.splitla implementación de referencia en Java 7 y Java 8. El código se recupera 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<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);
}

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

Mantener la compatibilidad

Siguiendo el comportamiento en Java 8 y superior

Para que se splitcomporte de manera uniforme en todas las versiones y sea compatible con el comportamiento en Java 8:

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

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

Siguiendo el comportamiento en Java 7 y anteriores

No existe una solución general para hacer splitcompatible con versiones anteriores de Java 7 y versiones anteriores, salvo reemplazar todas las instancias de splitpara que apunten a su propia implementación personalizada.

nhahtdh
fuente
¿Alguna idea de cómo puedo cambiar el split("")código para que sea coherente en las diferentes versiones de Java?
Daniel
2
@Daniel: Es posible hacerlo compatible con versiones posteriores (siga el comportamiento de Java 8) agregando (?!^)al final de la expresión regular y envolviendo la expresión regular original en un grupo que no captura (?:...)(si es necesario), pero no puedo pensar en ninguna forma de hacerlo compatible con versiones anteriores (siga el comportamiento anterior en Java 7 y anteriores).
nhahtdh
Gracias por la explicación. ¿Podrías describirlo "(?!^)"? ¿En qué escenarios será diferente ""? (¡Soy terrible en regex!: - /).
Daniel
1
@Daniel: su significado se ve afectado por la Pattern.MULTILINEbandera, mientras que \Asiempre coincide al principio de la cadena independientemente de las banderas.
nhahtdh
30

Esto se ha 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 una subcadena inicial vacía.

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

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

(Se eliminó el código fuente irrelevante)

Alexis C.
fuente
3
Sólo es una pregunta. ¿Está bien publicar un fragmento de código del JDK? ¿Recuerda el problema de los derechos de autor con Google - Harry Potter - Oracle?
Paul Vargas
6
@PaulVargas Para ser justos, no lo sé, pero supongo que está bien, ya que puedes descargar el JDK y descomprimir el archivo src que contiene todas las fuentes. Entonces, técnicamente, todo el mundo podía ver la fuente.
Alexis C.
12
@PaulVargas El "abierto" en "código abierto" significa algo.
Marko Topolnik
2
@ZouZou: solo porque todos puedan verlo no significa que pueda volver a publicarlo
user102008
2
@Paul Vargas, IANAL, pero en muchas otras ocasiones este tipo de publicación se encuentra en una situación de cita / uso justo. Más sobre el tema está aquí: meta.stackexchange.com/questions/12527/…
Alex Pakka
14

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

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 una subcadena inicial vacía.

(énfasis mío)

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

arshajii
fuente
Unos segundos más marcaron la diferencia.
Paul Vargas
2
@PaulVargas en realidad aquí arshajii publicó la respuesta unos segundos antes que ZouZou, pero desafortunadamente ZouZou respondió a mi pregunta anteriormente aquí . Me preguntaba si debería hacer esta pregunta ya que ya sabía una respuesta, pero parecía interesante y ZouZou merecía algo de reputación por su comentario anterior.
Pshemo
5
A pesar de que el nuevo comportamiento parece más lógico , obviamente se trata de una ruptura de compatibilidad con versiones anteriores . La única justificación para este cambio es que "some-string".split("")es un caso bastante raro.
ivstas
4
.split("")no es la única forma de dividir sin hacer coincidir nada. Usamos una expresión regular de búsqueda anticipada positiva que en jdk7, que también coincidió al principio y produjo un elemento de cabeza vacío que ahora se ha ido. github.com/spray/spray/commit/…
jrudolph