web-dev-qa-db-fra.com

TreeSet affiche une sortie incorrecte

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?

43
Show Stopper

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’interface Comparable ou l’interface Comparator pour une définition précise de compatible avec equals.) Il en est ainsi car l’interface Set est définie en fonction de l’opération equals, mais un ensemble trié effectue toutes les comparaisons d’éléments à l’aide de son compareTo (ou compare), 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'interface Set

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 si e1.compareTo(e2) == 0 a la même valeur booléenne que e1.equals(e2) pour chaque e1 et e2 de la classe C. Notez que null n'est pas une instance d'une classe et que e.compareTo(null) doit lancer une NullPointerException même si e.equals(null) renvoie false.

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).

41
VGR

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.

15
Shadov

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.

3
Arnaud

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.

La source

0
Tiyeb B