首先我们来看看,Object类的equals方法是怎样写的:
public boolean equals(Object obj) {
return (this == obj);
}
咦,不对啊,不是说好了==是比较地址,equals()是比较值么?这句话其实没说全,完整版是:使用equals()比较八大包装类对象和String类对象时,比较的是值;而对于其它对象,默认比较的是引用地址。 为什么String类对象才是比较值呢?因为人家鸡贼啊,人家默默地重写了equals方法!
- String类重写的equals方法
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
//地址不一致再看是否此类对象
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//对比值
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
- equals方法重写模版(先判地址再判空再判类型)
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
People p = (People)obj;
return this.name.equals(p.name)
&& this.age == p.age;
}
- 简洁版
public boolean equals(Object o) {
if (o == null || !(o instanceof Key))
{ return false; }
else
...
}
嗯,这个我懂,但是为什么重写了equals方法之后,一般都要把hashCode方法也重写了呢?这就需要咱先弄懂,hashCode()是用来干什么的。
众所周知,HashMap/Hashtable的key(HashSet)是不允许重复的,那么,当向HashSet中插入对象时,如何判别是否已经存在该对象了呢?(这里说的是equals和hashCode方法均未重写的情况)
1.调用这个对象的hashCode(),得到对应的hash值,看是否已有此hash值(哈希值由一个table维护),若无,则直接存储;
2.若存在此hash值,便调用equals()进行比较,若不同则存储,相同则覆盖。
hashCode方法就是根据一定的规则将与对象相关的信息(如存储地址、字段等)映射成一个数值,这个数值称作为散列值。
总结一下,判断两个对象相等,先hashCode()后equals()。亦即当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true。
但是现在问题是,你重写了equals(),使equals方法从地址判断变成了值判断,那有没有可能有两个对象,值相等而hashCode()却不同的呢?
哈希值是由地址和字段等因子决定的,我们在这里简化一下,直接把哈希值等同于地址(有的JVM就是这样的)。那么,上面的问题就变成了:是否有两个对象,值相等而地址不一致?答案是肯定的。再转化一下,就是equals()返回true,但是hashCode()不同。这就尴尬了,说好的hashCode()通过了再equals()呢?说好的obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()呢?所以,为了保持逻辑上的一致,就需要重写hashCode()。
- String类重写的hashCode方法
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
如何重写hashCode方法呢?一般来说,就是依赖字段(放弃地址)来生成哈希码,从而保持逻辑上的一致性。
- HashMap类的put方法(先检验hashCode()后equals())
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- HashMap类的get方法(依赖哈希码来get,哈希码相同则判equals())
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
问题又来了,比如说我有一个People类,有name,age,sex,city字段,在重写hashCode()时,把这4个字段都作为哈希值的因子。后来我调用put(peopleMe,“我”)把对象peopleMe插进了一个HashMap。但当我长了1岁,age+1,这时哈希值显然是要变的,虽然我还是我,但是调用get(peopleMe)时,由于哈希值变了,已经找不到“我”了。所以,在重写hashCode()和equals()时,尽量不要依赖易变的字段。