Элемент присутствует, но `Set.contains(element)` возвращает false

Как элемент не может содержаться в исходном наборе, но в его немодифицированной копии?

Исходный набор не содержит элемент во время его копирования. Смотрите изображение.

Следующий метод возвращает true, хотя он всегда должен возвращать false. Реализация c и clusters выполняется в обоих случаях HashSet.

public static boolean confumbled(Set<String> c, Set<Set<String>> clusters) {
    return (!clusters.contains(c) && new HashSet<>(clusters).contains(c));
}

Отладка показала, что элемент содержится в оригинале, но Set.contains(element) возвращает false по какой-либо причине. Смотрите изображение.

Может кто-нибудь объяснить мне, что происходит?

Ответ 1

Если вы измените элемент в Set (в вашем случае элементы Set<String>, поэтому добавление или удаление String изменит их), Set.contains(element) может не найти его, так как hashCode элемент будет отличаться от того, что было, когда элемент был сначала добавлен в HashSet.

Когда вы создаете новый HashSet, содержащий элементы исходного, элементы добавляются на основе их текущего hashCode, поэтому Set.contains(element) вернет true для нового HashSet.

Вам следует избегать размещения изменяемых экземпляров в HashSet (или использовать их как ключи в HashMap), и если вы не можете избежать этого, убедитесь, что вы удалили элемент перед его мутацией и повторно добавили это потом. В противном случае ваш HashSet будет сломан.

Пример:

Set<String> set = new HashSet<String>();
set.add("one");
set.add("two");
Set<Set<String>> setOfSets = new HashSet<Set<String>>();
setOfSets.add(set);
boolean found = setOfSets.contains(set); // returns true
set.add("three");
Set<Set<String>> newSetOfSets = new HashSet<Set<String>>(setOfSets);
found = setOfSets.contains(set); // returns false
found = newSetOfSets.contains(set); // returns true

Ответ 2

Наиболее распространенной причиной этого является то, что элемент или ключ были изменены после вставки, что привело к повреждению базовой структуры данных.

Примечание: при добавлении ссылки на Set<String> в другую Set<Set<String>> вы добавляете копию ссылки, базовый Set<String> не копируется, и если вы измените его, эти изменения, которые влияют на Set<Set<String>> вы положили его.

например.

Set<String> s = new HashSet<>();
Set<Set<String>> ss = new HashSet<>();
ss.add(s);
assert ss.contains(s);

// altering the set after adding it corrupts the HashSet
s.add("Hi");
// there is a small chance it may still find it.
assert !ss.contains(s);

// build a correct structure by copying it.
Set<Set<String>> ss2 = new HashSet<>(ss);
assert ss2.contains(s);

s.add("There");
// not again.
assert !ss2.contains(s);

Ответ 3

Если основной Set был TreeSet (или, возможно, другой NavigableSet), тогда возможно, если ваши объекты будут неадекватно сопоставлены, чтобы это произошло.

Критическая точка заключается в том, что HashSet.contains выглядит следующим образом:

public boolean contains(Object o) {
    return map.containsKey(o);
}

и map - это HashMap, а HashMap.containsKey выглядит следующим образом:

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

поэтому он использует ключ hashCode для проверки наличия.

A TreeSet однако использует внутри TreeMap, а containsKey выглядит следующим образом:

final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    if (comparator != null)
        return getEntryUsingComparator(key);
    ...

Поэтому для поиска ключа используется Comparator.

Итак, в общем, если ваш метод hashCode не согласен с вашим методом Comparator.compareTo (скажем, compareTo возвращает 1, а hashCode возвращает разные значения), вы увидите это неясное поведение.

class BadThing {

    final int hash;

    public BadThing(int hash) {
        this.hash = hash;
    }

    @Override
    public int hashCode() {
        return hash;
    }

    @Override
    public String toString() {
        return "BadThing{" + "hash=" + hash + '}';
    }

}

public void test() {
    Set<BadThing> primarySet = new TreeSet<>(new Comparator<BadThing>() {

        @Override
        public int compare(BadThing o1, BadThing o2) {
            return 1;
        }
    });
    // Make the things.
    BadThing bt1 = new BadThing(1);
    primarySet.add(bt1);
    BadThing bt2 = new BadThing(2);
    primarySet.add(bt2);
    // Make the secondary set.
    Set<BadThing> secondarySet = new HashSet<>(primarySet);
    // Have a poke around.
    test(primarySet, bt1);
    test(primarySet, bt2);
    test(secondarySet, bt1);
    test(secondarySet, bt2);
}

private void test(Set<BadThing> set, BadThing thing) {
    System.out.println(thing + " " + (set.contains(thing) ? "is" : "NOT") + " in <" + set.getClass().getSimpleName() + ">" + set);
}

печатает

BadThing{hash=1} NOT in <TreeSet>[BadThing{hash=1}, BadThing{hash=2}]
BadThing{hash=2} NOT in <TreeSet>[BadThing{hash=1}, BadThing{hash=2}]
BadThing{hash=1} is in <HashSet>[BadThing{hash=1}, BadThing{hash=2}]
BadThing{hash=2} is in <HashSet>[BadThing{hash=1}, BadThing{hash=2}]

так что хотя объект есть в TreeSet, он не находит его, потому что компаратор никогда не возвращает 0. Однако, как только он находится в HashSet, все нормально, потому что HashSet использует hashCode, чтобы найти его, и они ведут себя корректно.