【effective Java读书笔记】对于所有对象都通用的方法(二)

时间:2022-11-02 16:02:06

一、覆盖equals时总要覆盖hashCode

equals上节讲完之后,似乎比较两个对象的时候自己覆盖equals就非常好用了。然而,如果仅仅只是覆盖equals在HashMap中使用的时候会出现意料之外的结果。

如下代码:只覆盖了equals,没有覆盖hashCode方法。

public class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(short areaCode, short prefix, short lineNumber) {
super();
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
}
public PhoneNumber(int i, int j, int k) {
this.areaCode = (short)i;
this.prefix = (short)j;
this.lineNumber = (short)k;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof PhoneNumber)) {
return false;
}
PhoneNumber pn = (PhoneNumber) obj;
return pn.areaCode==areaCode&&pn.prefix==prefix&&pn.lineNumber==lineNumber;
}

}
执行代码一:使用HashMap执行结果输出为null
@org.junit.Testpublic void test() {Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();m.put(new PhoneNumber(707, 867, 5309), "Jenny");//取出结果为nullSystem.out.println(m.get(new PhoneNumber(707, 867, 5309)));}
执行代码二:使用ArrayList执行结果输出为true

@org.junit.Test
public void test1() {
List<PhoneNumber> m = new ArrayList<PhoneNumber>();
m.add(new PhoneNumber(707, 867, 5309));
//结果为true
System.out.println(m.contains(new PhoneNumber(707, 867, 5309)));
}
执行代码一是没有put进去这个对象么?不。debug发现已经加入了HashMap;

【effective Java读书笔记】对于所有对象都通用的方法(二)

然后看看问题出在哪?猜想可知出在HashMap的get方法,看看源码:

public V get(Objectkey) {

        Node<K,V>e;

        return (e = getNode(hash(key),key)) ==null ? null : e.value;

    }

hash(key)源码如下:

staticfinal int hash(Object key) {

        int h;

        return (key ==null) ? 0 : (h =key.hashCode()) ^ (h >>> 16);

    }

调用了HashMap的getNode方法;

//hash等于传入对象的hash值,key是指传入对象
final Node<K,V> getNode(int hash, Object key) {
//Node数组tab,Node值first
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//table中数据给tab一份,first等于tab的最后一个值和hash值都不为空才等于tab的最后一个值;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//first.hash如果等于传入对象的hash值,才继续,如果再满足对象引用相等或者对象值相等返回first对象
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果first.next不为空
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//e.hash如果等于传入对象的hash值,才继续,如果再满足对象引用相等或者对象值相等返回e对象
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
上述源码写了详细的注释,判断这个对象new PhoneNumber(707, 867, 5309)是否在m中,断点发现第一个条件不满足:

(tab =table) != null && (n =tab.length) > 0 &&

            (first =tab[(n - 1) &hash]) !=null

然后再看上面加入的时候对象的hashCode是1766751238,而取出的时候hashCode是1174361318,导致两个相等对象不同的hashCode码。所以返回null。根据getNode的源码,发现HashMap自身还是有优化的,先去判断HashCode是否相等,如果不相等就不继续比较了,也就不存在equals方法比较了。棒棒哒!

解决这个问题非常好解决,只需要将对象相等的对象hashCode相等即可。在PhoneNumber中加入下面代码:

@Override

publicint hashCode() {

return 42;

}

返回一个固定值,那么是否很完美?不,这种算法那么就相当于将所有数据都塞入一个散列桶里了。将Hash的优化给干没了,性能也就相当于list的算法。

如何写出一个好的散列码呢?需遵守“为不相等的对象产生不相等的散列码”。

当然,此处我一般都用现代ide提供的自动生成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;
}

二、始终要覆盖toString

至于问为什么?看个例子,

@org.junit.Test
public void test2() {
List<PhoneNumber> m = new ArrayList<PhoneNumber>();
m.add(new PhoneNumber(707, 867, 5309));
System.out.println(m.toString());
}
如果没覆盖得到的结果:

[hashTest.PhoneNumber@2a]

如果覆盖得到的结果:

[PhoneNumber [areaCode=707, prefix=867, lineNumber=5309]]