Por que em Java 8 a divisão às vezes remove strings vazias no início da matriz de resultado?


110

Antes do Java 8, quando dividimos em strings vazias, como

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

mecanismo de divisão seria dividido em locais marcados com |

|a|b|c|

porque o espaço vazio ""existe antes e depois de cada personagem. Então, como resultado, ele geraria primeiro este array

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

e mais tarde removerá strings vazias (porque não fornecemos explicitamente um valor negativo para o limitargumento), então ele finalmente retornará

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

No Java 8, o mecanismo de divisão parece ter mudado. Agora, quando usamos

"abc".split("")

obteremos ["a", "b", "c"]array em vez de, ["", "a", "b", "c"]então, parece que strings vazias no início também são removidas. Mas esta teoria falha porque, por exemplo

"abc".split("a")

retorna a matriz com uma string vazia no início ["", "bc"].

Alguém pode explicar o que está acontecendo aqui e como as regras de divisão mudaram no Java 8?


Java8 parece consertar isso. Enquanto isso, s.split("(?!^)")parece funcionar.
shkschneider

2
@shkschneider O comportamento descrito na minha pergunta não é um bug das versões anteriores ao Java-8. Esse comportamento não foi particularmente útil, mas ainda estava correto (como mostrado na minha pergunta), então não podemos dizer que foi "consertado". Eu vejo isso mais como melhoria para que pudéssemos utilizar split("")em vez de enigmática (para as pessoas que não usam regex) split("(?!^)")ou split("(?<!^)")ou alguns outros regexes.
Pshemo de

1
Encontrei o mesmo problema depois de atualizar o fedora para o Fedora 21, o fedora 21 vem com o JDK 1.8 e meu aplicativo de jogo IRC está quebrado por causa disso.
LiuYan 刘 研

7
Esta pergunta parece ser a única documentação dessa alteração significativa no Java 8. A Oracle a deixou de fora de sua lista de incompatibilidades .
Sean Van Gorder

4
Essa mudança no JDK me custou apenas 2 horas rastreando o que está errado. O código funciona bem no meu computador (JDK8), mas falha misteriosamente em outra máquina (JDK7). O Oracle REALMENTE DEVE atualizar a documentação de String.split (String regex) , em vez de Pattern.split ou String.split (String regex, int limit), pois esse é de longe o uso mais comum. Java é conhecido por sua portabilidade, também conhecida como WORA. Esta é uma grande mudança retrógrada e não está bem documentada.
PoweredByRice,

Respostas:


84

O comportamento de String.split(que chama Pattern.split) muda entre Java 7 e Java 8.

Documentação

Comparando entre a documentação de Pattern.splitem Java 7 e Java 8 , observamos a seguinte cláusula que está sendo adicionado:

Quando há uma correspondência de largura positiva no início da sequência de entrada, uma substring inicial vazia é incluída no início da matriz resultante. Uma correspondência de largura zero no início, entretanto, nunca produz essa substring inicial vazia.

A mesma cláusula também é adicionada String.splitno Java 8 , em comparação com o Java 7 .

Implementação de referência

Vamos comparar o código da Pattern.splitimplementação de referência em Java 7 e Java 8. O código é recuperado de grepcode, para as versões 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);
}

A adição do código a seguir em Java 8 exclui a correspondência de comprimento zero no início da string de entrada, o que explica o comportamento acima.

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

Manter a compatibilidade

Seguindo o comportamento no Java 8 e superior

Para tornar o splitcomportamento consistente entre as versões e compatível com o comportamento em Java 8:

  1. Se sua regex pode corresponder a uma string de comprimento zero, basta adicionar (?!\A)no final da regex e envolver a regex original no grupo de não captura (?:...)(se necessário).
  2. Se sua regex não pode corresponder a uma string de comprimento zero, você não precisa fazer nada.
  3. Se você não sabe se a regex pode corresponder a uma string de comprimento zero ou não, execute as duas ações da etapa 1.

(?!\A) verifica se a string não termina no início da string, o que implica que a correspondência é uma correspondência vazia no início da string.

Seguindo o comportamento em Java 7 e anteriores

Não há uma solução geral para tornar splitcompatível com versões anteriores do Java 7 e anteriores, exceto substituir todas as instâncias de splitpara apontar para sua própria implementação customizada.


Alguma ideia de como posso alterar o split("")código para que seja consistente nas diferentes versões de java?
Daniel

2
@Daniel: É possível torná-lo compatível com versões futuras (siga o comportamento do Java 8) adicionando (?!^)ao final do regex e envolvendo o regex original em um grupo de não captura (?:...)(se necessário), mas não consigo pensar em nenhum maneira de torná-lo compatível com versões anteriores (siga o comportamento antigo em Java 7 e anteriores).
nhahtdh

Obrigada pelo esclarecimento. Você poderia descrever "(?!^)"? Em que cenários será diferente ""? (Eu sou péssimo em regex!: - /).
Daniel,

1
@Daniel: Seu significado é afetado pela Pattern.MULTILINEbandeira, embora \Asempre corresponda ao início da string, independentemente das bandeiras.
nhahtdh

30

Isso foi especificado na documentação de split(String regex, limit).

Quando há uma correspondência de largura positiva no início desta string, uma substring inicial vazia é incluída no início da matriz resultante. Uma correspondência de largura zero no início, entretanto, nunca produz essa substring inicial vazia.

Em, "abc".split("")você obteve uma correspondência de largura zero no início, de modo que a substring vazia inicial não é incluída na matriz resultante.

No entanto, em seu segundo snippet, ao dividir, "a"você obteve uma correspondência de largura positiva (1 neste caso), portanto, a substring inicial vazia é incluída conforme o esperado.

(Código-fonte irrelevante removido)


3
É só uma pergunta. Posso postar um fragmento de código do JDK? Lembra do problema de direitos autorais com o Google - Harry Potter - Oracle?
Paul Vargas

6
@PaulVargas Para ser justo, não sei, mas presumo que esteja tudo bem, pois você pode baixar o JDK e descompactar o arquivo src que contém todas as fontes. Então, tecnicamente, todos poderiam ver a fonte.
Alexis C.

12
@PaulVargas O "aberto" em "código aberto" representa alguma coisa.
Marko Topolnik

2
@ZouZou: só porque todos podem ver, não significa que você pode publicá-lo
novamente

2
@Paul Vargas, IANAL mas em muitas outras ocasiões este tipo de postagem se enquadra na situação de cotação / uso justo. Mais sobre o assunto está aqui: meta.stackexchange.com/questions/12527/…
Alex Pakka

14

Houve uma pequena mudança nos documentos split()de Java 7 para Java 8. Especificamente, a seguinte instrução foi adicionada:

Quando há uma correspondência de largura positiva no início desta string, uma substring inicial vazia é incluída no início da matriz resultante. Uma correspondência de largura zero no início, entretanto, nunca produz essa substring inicial vazia.

(ênfase minha)

A divisão da string vazia gera uma correspondência de largura zero no início, portanto, uma string vazia não é incluída no início da matriz resultante de acordo com o que é especificado acima. Por outro lado, seu segundo exemplo que se divide "a"gera uma correspondência de largura positiva no início da string, de modo que uma string vazia é de fato incluída no início do array resultante.


Mais alguns segundos fizeram a diferença.
Paul Vargas

2
@PaulVargas na verdade aqui arshajii postou a resposta alguns segundos antes de ZouZou, mas infelizmente ZouZou respondeu minha pergunta anteriormente aqui . Eu queria saber se eu deveria fazer essa pergunta, pois já sabia a resposta, mas parecia interessante e ZouZou merecia alguma reputação por seu comentário anterior.
Pshemo

5
Apesar do novo comportamento parecer mais lógico , é obviamente uma quebra de compatibilidade com versões anteriores . A única justificativa para essa mudança é que "some-string".split("")é um caso bastante raro.
ivstas

4
.split("")não é a única maneira de dividir sem combinar nada. Usamos uma regex lookahead positiva que em jdk7, que também correspondeu no início e produziu um elemento head vazio que agora se foi. github.com/spray/spray/commit/…
jrudolph
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.