Pourquoi dans Java 8, split supprime parfois les chaînes vides au début du tableau de résultats?

110

Avant Java 8 lorsque nous nous sommes séparés sur une chaîne vide comme

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

le mécanisme de division se diviserait aux endroits marqués |

|a|b|c|

car un espace vide ""existe avant et après chaque caractère. Donc, en conséquence, il générerait d'abord ce tableau

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

et plus tard, supprimera les chaînes vides de fin (car nous n'avons pas explicitement fourni de valeur négative à l' limitargument) afin qu'il retourne finalement

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

Dans Java 8, le mécanisme de partage semble avoir changé. Maintenant quand nous utilisons

"abc".split("")

nous obtiendrons un ["a", "b", "c"]tableau au lieu de ["", "a", "b", "c"]cela, il semblerait que les chaînes vides au début soient également supprimées. Mais cette théorie échoue parce que par exemple

"abc".split("a")

renvoie un tableau avec une chaîne vide au début ["", "bc"].

Quelqu'un peut-il expliquer ce qui se passe ici et comment les règles de partage ont changé dans Java 8?

Pshemo
la source
Java8 semble résoudre ce problème. Pendant ce temps, s.split("(?!^)")semble fonctionner.
shkschneider
2
@shkschneider Le comportement décrit dans ma question n'est pas un bogue des versions antérieures à Java-8. Ce comportement n'était pas particulièrement très utile, mais il était quand même correct (comme le montre ma question), nous ne pouvons donc pas dire qu'il était "corrigé". Je le vois plus comme une amélioration afin que nous puissions utiliser au split("")lieu de cryptique (pour les personnes qui n'utilisent pas regex) split("(?!^)")ou split("(?<!^)")ou quelques regexes d'autres.
Pshemo
1
J'ai rencontré le même problème après la mise à niveau de fedora vers Fedora 21, fedora 21 est livré avec JDK 1.8, et mon application de jeu IRC est cassée à cause de cela.
LiuYan 刘 研
7
Cette question semble être la seule documentation de ce changement de rupture dans Java 8. Oracle l'a laissé hors de sa liste d'incompatibilités .
Sean Van Gorder
4
Ce changement dans le JDK m'a coûté juste 2 heures de recherche de ce qui ne va pas. Le code fonctionne correctement sur mon ordinateur (JDK8) mais échoue mystérieusement sur une autre machine (JDK7). Oracle DEVRAIT VRAIMENT mettre à jour la documentation de String.split (String regex) , plutôt que de Pattern.split ou String.split (String regex, int limit) car c'est de loin l'utilisation la plus courante. Java est connu pour sa portabilité, également appelée WORA. Il s'agit d'un changement majeur en arrière et pas du tout bien documenté.
PoweredByRice

Réponses:

84

Le comportement de String.split(qui appelle Pattern.split) change entre Java 7 et Java 8.

Documentation

En comparant la documentation de Pattern.splitdans Java 7 et Java 8 , nous observons l'ajout de la clause suivante:

Lorsqu'il existe une correspondance de largeur positive au début de la séquence d'entrée, une sous-chaîne de début vide est incluse au début du tableau résultant. Une correspondance de largeur nulle au début ne produit cependant jamais une telle sous-chaîne de début vide.

La même clause est également ajoutée String.splitdans Java 8 , par rapport à Java 7 .

Implémentation de référence

Comparons le code de Pattern.splitl'implémentation de référence en Java 7 et Java 8. Le code est récupéré depuis grepcode, pour les versions 7u40-b43 et 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'ajout du code suivant dans Java 8 exclut la correspondance de longueur nulle au début de la chaîne d'entrée, ce qui explique le comportement ci-dessus.

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

Maintenir la compatibilité

Comportement suivant dans Java 8 et supérieur

Pour rendre le splitcomportement cohérent entre les versions et compatible avec le comportement de Java 8:

  1. Si votre expression régulière peut correspondre à une chaîne de longueur nulle, ajoutez simplement (?!\A)à la fin de l'expression régulière et enveloppez l'expression régulière d'origine dans un groupe non capturant (?:...)(si nécessaire).
  2. Si votre expression régulière ne peut pas correspondre à une chaîne de longueur nulle, vous n'avez rien à faire.
  3. Si vous ne savez pas si l'expression régulière peut correspondre à une chaîne de longueur nulle ou non, effectuez les deux actions de l'étape 1.

(?!\A) vérifie que la chaîne ne se termine pas au début de la chaîne, ce qui implique que la correspondance est une correspondance vide au début de la chaîne.

Comportement suivant dans Java 7 et antérieurs

Il n'y a pas de solution générale pour rendre la splitcompatibilité descendante avec Java 7 et les versions antérieures, à moins de remplacer toutes les instances de splitpour pointer vers votre propre implémentation personnalisée.

nhahtdh
la source
Une idée de la façon dont je peux changer le split("")code pour qu'il soit cohérent entre les différentes versions de Java?
Daniel
2
@Daniel: Il est possible de le rendre compatible vers l'avant (suivez le comportement de Java 8) en ajoutant (?!^)à la fin de l'expression régulière et en enveloppant l'expression régulière d'origine dans un groupe non capturant (?:...)(si nécessaire), mais je ne peux penser à aucun moyen de le rendre rétrocompatible (suivez l'ancien comportement de Java 7 et antérieurs).
nhahtdh
Merci pour l'explication. Pouvez-vous décrire "(?!^)"? Dans quels scénarios sera-t-il différent ""? (Je suis terrible en regex!: - /).
Daniel
1
@Daniel: Sa signification est affectée par l' Pattern.MULTILINEindicateur, alors qu'il \Acorrespond toujours au début de la chaîne indépendamment des indicateurs.
nhahtdh
30

Cela a été spécifié dans la documentation de split(String regex, limit).

Lorsqu'il existe une correspondance de largeur positive au début de cette chaîne, une sous-chaîne de début vide est incluse au début du tableau résultant. Une correspondance de largeur nulle au début ne produit cependant jamais une telle sous-chaîne de début vide.

Dans "abc".split("")vous avez une correspondance de largeur nulle au début, de sorte que la sous-chaîne vide de début n'est pas incluse dans le tableau résultant.

Cependant, dans votre deuxième extrait de code lorsque vous vous divisez, "a"vous avez une correspondance de largeur positive (1 dans ce cas), de sorte que la sous-chaîne de début vide est incluse comme prévu.

(Suppression du code source non pertinent)

Alexis C.
la source
3
C'est juste une question. Est-il acceptable de publier un fragment de code à partir du JDK? Vous vous souvenez du problème de copyright avec Google - Harry Potter - Oracle?
Paul Vargas
6
@PaulVargas Pour être honnête, je ne sais pas mais je suppose que c'est correct puisque vous pouvez télécharger le JDK et décompresser le fichier src qui contient toutes les sources. Donc, techniquement, tout le monde pouvait voir la source.
Alexis C.
12
@PaulVargas Le "open" dans "open source" signifie quelque chose.
Marko Topolnik
2
@ZouZou: ce n'est pas parce que tout le monde peut le voir que vous pouvez le
republier
2
@Paul Vargas, IANAL mais dans de nombreuses autres occasions, ce type de message relève de la situation de devis / utilisation équitable. Plus d'informations sur le sujet ici: meta.stackexchange.com/questions/12527/…
Alex Pakka
14

Il y a eu un léger changement dans la documentation pour passer split()de Java 7 à Java 8. Plus précisément, la déclaration suivante a été ajoutée:

Lorsqu'il existe une correspondance de largeur positive au début de cette chaîne, une sous-chaîne de début vide est incluse au début du tableau résultant. Une correspondance de largeur nulle au début ne produit cependant jamais une telle sous-chaîne de début vide.

(c'est moi qui souligne)

Le fractionnement de chaîne vide génère une correspondance de largeur nulle au début, donc une chaîne vide n'est pas incluse au début du tableau résultant conformément à ce qui est spécifié ci-dessus. En revanche, votre deuxième exemple qui se divise sur "a"génère une correspondance de largeur positive au début de la chaîne, donc une chaîne vide est en fait incluse au début du tableau résultant.

arshajii
la source
Quelques secondes de plus ont fait la différence.
Paul Vargas
2
@PaulVargas en fait ici, arshajii a posté une réponse quelques secondes avant ZouZou, mais malheureusement ZouZou a répondu à ma question plus tôt ici . Je me demandais si je devais poser cette question car je connaissais déjà une réponse mais elle me paraissait intéressante et ZouZou méritait une certaine réputation pour son commentaire précédent.
Pshemo
5
Bien que le nouveau comportement semble plus logique , il s'agit évidemment d'une rupture de compatibilité descendante . La seule justification de ce changement est qu'il "some-string".split("")s'agit d'un cas assez rare.
ivstas
4
.split("")n'est pas le seul moyen de se séparer sans rien égaler. Nous avons utilisé une expression régulière lookahead positive qui, dans jdk7, correspondait également au début et produisait un élément head vide qui a maintenant disparu. github.com/spray/spray/commit/…
jrudolph