Элемент присутствует, но `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
, чтобы найти его, и они ведут себя корректно.