TreeSet pokazuje złe wyniki

Podczas pracy z zestawem drzew zauważyłem bardzo osobliwe zachowanie. Zgodnie z moim zrozumieniem ten program powinien drukować dwie identyczne linie:

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

Ale o dziwo to drukuje:

[b]
[a, b] 

Dlaczego zestaw drzew tak się zachowuje?

Author: John Kugelman, 2017-05-11

4 answers

Dzieje się tak, ponieważ komparator SortedSet jest używany do sortowania, ale removeAll opiera się na metodzie equals każdego elementu. Z SortedSet dokumentacji :

Zauważ, że kolejność utrzymywana przez sortowany zestaw (niezależnie od tego, czy dostarczony jest jawny komparator) musi być zgodna z równymi , jeśli sortowany zestaw ma poprawnie zaimplementować interfejs Set. (Zobacz interfejs Comparable lub interfejs Comparator, aby uzyskać dokładną definicję zgodne z równymi.) jest tak dlatego, że interfejs Set jest zdefiniowany jako operacja equals, ale posortowany zbiór wykonuje wszystkie porównania elementów za pomocą metody compareTo (lub compare), więc dwa elementy, które są uważane za równe przez tę metodę, z punktu widzenia posortowanego zbioru, są równe. Zachowanie sortowanego zbioru jest dobrze zdefiniowane, nawet jeśli jego kolejność jest niezgodna z równymi; po prostu nie spełnia ogólnej Umowy interfejsu Set.

Wyjaśnienie "spójnego z równymi" jest zdefiniowane w porównywalnej dokumentacji :

Naturalny porządek dla klasy Cmówi się, że jest zgodny z równymi wtedy i tylko wtedy, gdy e1.compareTo(e2) == 0 ma taką samą wartość logiczną jak e1.equals(e2) dla każdego e1 i e2 klasy C. Zauważ, że null nie jest instancją żadnej klasy i e.compareTo(null) powinien rzucać NullPointerException, nawet jeśli e.equals(null) zwraca false.

Jest to zdecydowanie zalecane (choć nie jest to wymagane), aby naturalne porządki były zgodne z równymi. Dzieje się tak, ponieważ sortowane zbiory (i sortowane mapy) bez jawnych komparatorów zachowują się "dziwnie", gdy są używane z elementami (lub kluczami), których naturalna kolejność jest niezgodna z równymi. W szczególności taki posortowany zestaw (lub posortowana Mapa) narusza ogólną umowę dla zestawu (lub mapy), która jest zdefiniowana w kategoriach metody equals.

Podsumowując, komparator Twojego zestawu zachowuje się inaczej niż elements ' metoda equals, powodująca nietypowe (choć przewidywalne) zachowanie.

 41
Author: VGR,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-11 13:35:49

Cóż, to mnie zaskoczyło, Nie wiem czy mam rację, ale spójrz na tę implementację w 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;
}

Zasadniczo w twoim przykładzie rozmiar zestawu jest równy rozmiarowi argumentów, które chcesz usunąć, więc wywoływany jest warunek else. W tym warunku jest sprawdzanie, czy kolekcja argumentów do usunięcia contains bieżącego elementu iteratora, a to sprawdzanie jest wrażliwe na wielkość liter, więc sprawdza, czy c.contains("a") i zwraca false, ponieważ c zawiera "A", a nie "a", więc element nie jest usuwany. Zauważ, że dodanie elementu do zestawu s.addAll(Arrays.asList("a", "b", "d")); działa poprawnie, ponieważ size() > c.size() jest teraz prawdziwe, więc nie ma już contains sprawdzania.

 15
Author: Shadov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-11 17:46:30

Aby dodać kilka informacji o tym, dlaczego remove z TreeSet faktycznie usuwa wielkość liter w twoim przykładzie (i pod warunkiem, że podążasz ścieżką if (size() > c.size()), Jak wyjaśniono w odpowiedzi przez @ Shadov):

Jest to metoda removew TreeSet:

public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }

Wywołuje remove ze swojego wewnętrznego TreeMap:

public V remove(Object key) {
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

Które wywołują 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;
    }

Jeśli istnieje Comparator (jak w twoim przykładzie), wpis jest wyszukiwany na podstawie tego Comparator (odbywa się to przez getEntryUsingComparator), dlatego jest rzeczywiście znalezione (następnie usunięte), mimo różnicy przypadku.

 3
Author: Arnaud,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-11 14:26:38

To jest interesujące, więc oto kilka testów z wyjściem:

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] !!
}

Teraz bez komparatora działa jak sortowane HashSet<String>():

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

Teraz z dokumentacji:

Public boolean removeAll (Kolekcja c)

Usuwa z tego zbioru wszystkie jego elementy zawarte w określona kolekcja(operacja opcjonalna). Jeśli określony zbiór jest również zestawem, operacja ta skutecznie modyfikuje ten zestaw tak, aby jego wartość to asymetryczna różnica zestawu dwóch zestawów.

Ta implementacja określa, który jest mniejszy z tego zbioru i określonej kolekcji, wywołując na każdej z nich metodę size. Jeśli to set ma mniej elementów, wtedy implementacja iteruje nad tym set, sprawdzając po kolei każdy element zwracany przez iterator, aby sprawdzić, czy jest on zawarty w określonym zbiorze. Jeśli jest tak zawarta, to jest usuwany z tego zestawu za pomocą metody usuwania iteratora. Jeśli określone zbiór ma mniej elementów, wtedy implementacja iteruje nad określoną kolekcją, usuwając z tego zbioru każdy element zwracany przez iterator, używając metody remove tego zestawu.

Źródło

 0
Author: Tiyeb B,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-11 14:26:06