Pourquoi la suppression d'un TreeSet avec un comparateur personnalisé ne supprime-t-elle pas un plus grand ensemble d'éléments?

22

En utilisant à la fois Java 8 et Java 11, tenez compte des éléments suivants TreeSetavec un String::compareToIgnoreCasecomparateur:

final Set<String> languages = new TreeSet<>(String::compareToIgnoreCase);
languages.add("java");
languages.add("c++");
languages.add("python");

System.out.println(languages);                 // [c++, java, python]

Lorsque j'essaie de supprimer les éléments exacts présents dans le TreeSet, cela fonctionne: tous ceux spécifiés sont supprimés:

languages.removeAll(Arrays.asList("PYTHON", "C++"));

System.out.println(languages);                 // [java]

Cependant, si j'essaie de supprimer à la place plus que ce qui est présent dans le TreeSet, l'appel ne supprime rien du tout (ce n'est pas un appel ultérieur mais appelé à la place de l'extrait ci-dessus):

languages.removeAll(Arrays.asList("PYTHON", "C++", "LISP"));

System.out.println(languages);                 // [c++, java, python]

Qu'est-ce que je fais mal? Pourquoi se comporte-t-il de cette façon?

Modifier: String::compareToIgnoreCaseest un comparateur valide:

(l, r) -> l.compareToIgnoreCase(r)
Nikolas
la source
5
Entrée de bogue associée: bugs.openjdk.java.net/browse/JDK-8180409 (TreeSet removeAll comportement incohérent avec String.CASE_INSENSITIVE_ORDER)
Progman
Un Q& A étroitement lié .
Naman

Réponses:

22

Voici le javadoc de removeAll () :

Cette implémentation détermine laquelle est la plus petite de cet ensemble et de la collection spécifiée, en invoquant la méthode size sur chacune. Si cet ensemble contient moins d'éléments, l'implémentation itère sur cet ensemble, vérifiant tour à tour chaque élément renvoyé par l'itérateur pour voir s'il est contenu dans la collection spécifiée. S'il est ainsi contenu, il est supprimé de cet ensemble avec la méthode remove de l'itérateur. Si la collection spécifiée contient moins d'éléments, l'implémentation parcourt la collection spécifiée, supprimant de cet ensemble chaque élément renvoyé par l'itérateur, en utilisant la méthode remove de cet ensemble.

Dans votre deuxième expérience, vous êtes dans le premier cas du javadoc. Il itère donc sur "java", "c ++", etc. et vérifie s'ils sont contenus dans l'ensemble renvoyé par Set.of("PYTHON", "C++"). Ils ne le sont pas, ils ne sont donc pas supprimés. Utilisez un autre TreeSet utilisant le même comparateur que l'argument, et cela devrait fonctionner correctement. L'utilisation de deux implémentations Set différentes, l'une utilisant equals()l'autre et l'autre utilisant un comparateur, est une chose dangereuse à faire.

Notez qu'un bogue est ouvert à ce sujet: [JDK-8180409] TreeSet removeAll comportement incohérent avec String.CASE_INSENSITIVE_ORDER .

JB Nizet
la source
Voulez-vous dire que lorsque les deux ensembles auraient les mêmes caractéristiques, cela fonctionne? final Set<String> subLanguages = new TreeSet<>(String::compareToIgnoreCase); subLanguages.addAll(Arrays.asList("PYTHON", "C++", "LISP")); languages.removeAll(subLanguages);
Nikolas
1
Vous êtes dans le cas "Si cet ensemble contient moins d'éléments", décrit par le javadoc. L'autre cas étant "Si la collection spécifiée contient moins d'éléments".
JB Nizet
8
Cette réponse est juste, mais c'est un comportement très peu intuitif. Cela ressemble à une faille dans la conception de TreeSet.
Boann
Je suis d'accord, mais je ne peux rien y faire.
JB Nizet
4
C'est les deux: c'est un comportement très peu intuitif qui est correctement documenté, mais, étant peu intuitif et trompeur, c'est aussi un bug de conception qui pourrait, un jour, être corrigé.
JB Nizet