首先给出一个辅助类:
package disappearElementInHashset;
public class Person {
private String name;
public Person(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person [name=" + name + "]";
}
}
需要注意的是,这个类没有重写Object类的hashCode方法和equals方法。接下来给出一个测试类引出我们的主题:
package disappearElementInHashset;
import java.util.HashSet;
import java.util.Iterator;
public class Test {
public static void main(String[] args) {
HashSet<Person> personSet = new HashSet<>();
Person me = new Person("liyuncong");
personSet.add(me);
me.setName("wang");
personSet.add(me);
Iterator<Person> iterator = personSet.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println(personSet.size());
}
}
测试类的打印结果:
Person [name=wang]
1
我们往HashSet里放了两个元素,可是为什么只有一个元素呢,而且只有后面放进去的那个元素?(!!!提醒一下,后面这段解释是不对的)其实这是很好理解,因为集合中不能存在相等的元素,而me在修改前后是相等的,之所以me在修改前后是相等的,是因为Person并没有重写Object类的equals方法,Person实例的相等性比较,只是比较地址而已,而me指向的对象的值虽然被修改了,但是me始终都指向同一块空间。因此,第二个me并没有放进去。从HasHSet中打印出来的值成了me指向对象修改的值,是因为HashSet中保存的引用和me指向同一块地方。
这个解释听上去很有道理,但其实是不完整的。接下来,我们往Person里加入自定义的hashCode方法和equals方法。如下:
package disappearElementInHashset;
public class Person {
private String name;
public Person(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person [name=" + name + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
这时,我们再次运行上面的测试类,结果如下:
Person [name=wang]
Person [name=wang]
2
HashSet里面放了两个一样的元素,惊到我了。我们都敢确定,打印出来的两个对象是相等的,(它们不仅值相等,而且地址页相等,因为它们就是同一个对象,自始至终都只有一个对象,流浪在外的只是指向这个对象的多个指针),可是怎么都放进去了呢?这时,我觉得有必要看看HashSet的add方法的源码了。因为HashSet是基于HashMap实现的,于是,我们只需要看看HashMap的源码就行了,源码如下:
public V put(K key, V value) {
// 如果table之前为空,就创建一个更大的table
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果key为null,就用对应的put方法
if (key == null)
return putForNullKey(value);
// 计算key的哈希值
int hash = hash(key);
// 根据哈希值和table长度计算该键值对应该存储在那个桶中
int i = indexFor(hash, table.length);
// 如果这个桶中拥有键和正在添加的键值对的键相同的键值对,就只是更改这个已有的键值对得值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 如果这个桶中没有和正在添加的键值对的键相同的键值对,就把这个键值对添加到这个桶中
addEntry(hash, key, value, i);
return null;
}
这些明白了。HashSet是先通过对象的哈希值把对象找到相应的桶,然后再桶中寻找有没有相等对象。我们的程序中虽然自始至终都只有一个对象,时时刻刻都与自身相等,但是修改前后的哈希值却不同,第一次把指向对象的引用放在了一个桶中,而第二次却是在另一个桶中寻找是否已经存在相等的元素,就这样,程序中的结果产生了。
在最初的程序中,之所以只放进去了一个元素,是因为Object类的hashCode方法通过地址计算哈希值,地址没变,哈希值也就没变,于是第二个引用是试图放在和第一个引用同一个桶中,自然失败了。
程序中这样的问题其实是非常隐蔽的,所以,编码时,要关注各种引用之间千丝万缕的联系,还有关注自己类中equals方法和hashCode方法的定义。