Tout en travaillant avec un ensemble d'arbres, j'ai trouvé un comportement très particulier. Selon ma compréhension, ce programme devrait imprimer deux lignes identiques:
public class TestSet {
static void test(String... args) {
Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
s.addAll(Arrays.asList("a", "b"));
s.removeAll(Arrays.asList(args));
System.out.println(s);
}
public static void main(String[] args) {
test("A");
test("A", "C");
}
}
mais étrangement cela imprime:
[b]
[a, b]
Pourquoi les arbres se comportent-ils de la sorte?
Cela se produit car un comparateur SortedSet est utilisé pour le tri, mais removeAll repose sur la méthode equals
de chaque élément. À partir de la documentation SortedSet :
Notez que l'ordre maintenu par un ensemble trié (qu'un comparateur explicite soit fourni ou non) doit être cohérent avec égal si l'ensemble trié doit implémenter correctement l'interface
Set
. (Voir l’interfaceComparable
ou l’interfaceComparator
pour une définition précise de compatible avec equals.) Il en est ainsi car l’interfaceSet
est définie en fonction de l’opérationequals
, mais un ensemble trié effectue toutes les comparaisons d’éléments à l’aide de soncompareTo
(oucompare
), de sorte que deux éléments réputés égaux par cette méthode sont égaux, du point de vue de l'ensemble trié. Le comportement d'un ensemble trié est est bien défini même si son ordre est incohérent avec égal à égal; il ne parvient tout simplement pas à respecter le contrat général de l'interfaceSet
.
L'explication de «compatible avec égaux» est définie dans la Documentation comparable :
L'ordre naturel d'une classe
C
est dit cohérent avec equals si et seulement sie1.compareTo(e2) == 0
a la même valeur booléenne quee1.equals(e2)
pour chaquee1
ete2
de la classeC
. Notez quenull
n'est pas une instance d'une classe et quee.compareTo(null)
doit lancer uneNullPointerException
même sie.equals(null)
renvoiefalse
.Il est fortement recommandé (bien que non obligatoire) que les ordres naturels soient cohérents avec les égaux. En effet, les ensembles triés (et les cartes triées) sans comparateurs explicites se comportent "étrangement" lorsqu'ils sont utilisés avec des éléments (ou des clés) dont l'ordre naturel est incohérent avec égaux. En particulier, un tel ensemble trié (ou une carte triée) enfreint le contrat général pour un ensemble (ou une carte) défini dans la méthode
equals
.
En résumé, le comparateur de votre ensemble se comporte différemment de la méthode equals
des éléments, ce qui entraîne un comportement inhabituel (bien que prévisible).
Eh bien, cela m'a surpris, je ne sais pas si j'ai raison, mais regardez cette implémentation dans AbstractSet
:
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
if (size() > c.size()) {
for (Iterator<?> i = c.iterator(); i.hasNext(); )
modified |= remove(i.next());
} else {
for (Iterator<?> i = iterator(); i.hasNext(); ) {
if (c.contains(i.next())) {
i.remove();
modified = true;
}
}
}
return modified;
}
Fondamentalement, dans votre exemple, la taille de l'ensemble est égale à la taille des arguments que vous souhaitez supprimer. La condition else est donc appelée. Dans cette condition, il vérifie si votre collection d'arguments pour supprimer contains
l'élément actuel de l'itérateur, et cette vérification est sensible à la casse, afin de vérifier si c.contains("a")
et renvoie false, car c
contient "A"
, pas "a"
, de sorte que l'élément est pas enlevé. Notez que lorsque vous ajoutez un élément à votre ensemble s.addAll(Arrays.asList("a", "b", "d"));
, il fonctionne correctement, car size() > c.size()
est maintenant vrai, il n'y a donc plus de vérification contains
.
Pour ajouter des informations sur la raison pour laquelle la variable remove
de TreeSet
supprime réellement la casse dans votre exemple (et à condition que vous suiviez le chemin if (size() > c.size())
comme expliqué dans la réponse de @Shadov):
Ceci est la méthode remove
dans TreeSet
:
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
il appelle remove
à partir de son TreeMap
interne:
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
qui appelle getEntry
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
S'il existe une Comparator
(comme dans votre exemple), l'entrée est recherchée sur la base de cette Comparator
(cette opération est effectuée par getEntryUsingComparator
), c'est pourquoi elle est réellement trouvée (puis supprimée), malgré la différence de casse.
C'est intéressant, alors voici quelques tests avec sortie:
static void test(String... args) {
Set<String> s =new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
s.addAll(Arrays.asList( "a","b","c"));
s.removeAll(Arrays.asList(args));
System.out.println(s);
}
public static void main(String[] args) {
test("C"); output: [a, b]
test("C", "A"); output: [b]
test("C", "A","B"); output: [a, b, c]
test("B","C","A"); output: [a, b, c]
test("K","C"); output: [a, b]
test("C","K","M"); output: [a, b, c] !!
test("C","K","A"); output: [a, b, c] !!
}
Maintenant, sans le comparateur, cela fonctionne comme une HashSet<String>()
triée:
static void test(String... args) {
Set<String> s = new TreeSet<String>();//
s.addAll(Arrays.asList( "a","b","c"));
s.removeAll(Arrays.asList(args));
System.out.println(s);
}
public static void main(String[] args) {
test("c"); output: [a, b]
test("c", "a"); output: [b]
test("c", "a","b"); output: []
test("b","c","a"); output: []
test("k","c"); output: [a, b]
test("c","k","m"); output: [a, b]
test("c","k","m"); output: [a, b]
}
Maintenant de la documentation:
public boolean removeAll (Collection c)
Supprime de cet ensemble tous les éléments contenus dans le fichier collection spécifiée (opération facultative). Si la collection spécifiée est également un ensemble, cette opération modifie effectivement cet ensemble pour que sa valeur est la différence d'ensemble asymétrique des deux ensembles.
Cette implémentation détermine lequel est le plus petit de cet ensemble et la collection spécifiée, en invoquant la méthode de taille sur chacun. Si ce set a moins d’éléments, puis la mise en oeuvre itère dessus set, en vérifiant chaque élément retourné par l’itérateur à tour de rôle pour voir si il est contenu dans la collection spécifiée. Si c'est tellement contenu, ça est supprimé de cet ensemble avec la méthode remove de l'itérateur. Si la collection spécifiée a moins d'éléments, puis l'implémentation itère sur la collection spécifiée, en supprimant de cet ensemble chaque élément renvoyé par l'itérateur à l'aide de la méthode remove de cet ensemble.