hashCode,就是哈希值,可以理解为一个对象的标识(好的hash,能确保不同的对象有不同的hash值),Object含有hashCode方法,用来返回对象的hash值。hashCode方法多用在基于散列值的集合类,比如HashMap、HashSet和Hashtable。
下面是hashCode的约束规范,
在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,那么,对该对象调用hashCode方法多次,它必须返回同一个整数。在同一个应用多次执行过程中,这个整数可以不同。
如果两个对象根据equlas方法是相等的,那么调用这两个对象的hashCode方法必须产生同样的整数结果。
如果两个对象根据equals方法是不相等的。那么调用这两个对象的hashCode方法,不要求必须产生不同的整数结果。
如果你重写了类的equals方法,那么必须也重写hashCode方法。否则,就违反了上述的规范。这是因为,两个在逻辑上相等的对象(调用equals相等),必须拥有相同的hashCode,但是根据Object的hashCode,它们仅仅是两个对象,没有共同的地方。所以违背了规范2. 此时,我们就要重写hashCode方法。
实例代码1.
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point))
return false;
Point p = (Point) obj;
return p.x == x && p.y == y;
}
public static void main(String[] args) {
Point p1 = new Point(2, 5);
Point p2 = new Point(2, 5);
System.out.println("p1 equals p2? " + p1.equals(p2));
System.out.println(p1.hashCode());
System.err.println(p2.hashCode());
}
}
在上面的例子上,我们重写了equals方法,而且能成功判断p1和p2是相等的,但是没有重写hashCode方法,所以我们调用p1.hashCode和p2.hashCode返回的值是不一样的。
那么,hashCode方法应该是怎么样的呢?编写一个合法的hashCode并不难,比如,
public int hashCode() {
return 41;
}
由于hashCode规范,并没有要求不同的对象必须有不同的hashCode,所以我们可以给每个对象都返回一个相同的值。虽然这样,并没有违背hashCode的规范。但是在一些散列值存储中(HashSet、HashMap以及HashTable),却带来了灾难。
或许,你并不太了解散列值存储,我们以HashMap为例,HashMap提供了键值对(key-value)的存储,使用范例如下,
Point p1 = new Point(2, 5);
Point p2 = new Point(2, 5);
HashMap<Point, String> hm = new HashMap<Point, String>();
hm.put(p1, "p1");
hm.put(p2, "p2");
System.out.println(hm.get(p1));
System.out.println(hm.get(p2));
那么,hashCode对于HashMap的作用是什么呢?
我们知道,在HashMap中,不允许两个存在两个相同的对象,那么如何判断两个对象是否相等呢?你或许会说,肯定是调用equals,是的,调用equals没有问题,但是,如果HashMap含有数万条数据,对每个对象都调用equals方法,效率肯定是一个问题。
此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。下面这段代码是java.util.HashMap的中put方法的具体实现:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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;
}
put方法是用来向HashMap中添加新的元素,从put方法的具体实现可知,会先调用hashCode方法得到该元素的hashCode值,然后查看table中是否存在该hashCode值,如果存在则调用equals方法重新确定是否存在该元素,如果存在,则更新value值,否则将新的元素添加到HashMap中。从这里可以看出,hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率。
注意,HashMap在插入的时候,判断的是key的值是否相同。
问题来了,如果我们没有重写hashCode方法,那么即使对于两个相同的对象,hashCode的结果也是不一样的(例子1),那么往HashMap中插入数据的时候,就会重复插入(注意,此时的Point并没有实现hashCode方法),
Point p1 = new Point(2, 5);
Point p2 = new Point(2, 5);
HashMap<Point, String> hm = new HashMap<Point, String>();
hm.put(p1, "p1");
hm.put(p2, "p2");
System.out.println("HashMap size: " + hm.size());
System.out.println(hm.get(p1));
System.out.println(hm.get(p2));
运行程序,我们可以发现,HashMap的大小是2. p1和p2都可以从表中取出。
如果一个类是非可变的,并且计算hashCode的代价比较大,那么应该考虑把hashCode缓存在对象内部,而不是每次都重新计算,如果对于该类的大多数对象都被用于散列键,那么可以在实例被创建的时候就计算hashCode。否则的话,可以选择迟缓初始化hashCode,一直到hashCode第一次使用才初始化。
对于前者,代码可以如下,
private int hashCode;
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
/** * 在此处初始化hashCode */
}
@Override
public int hashCode() {
return hashCode();
}
在对象初始化的时候,就计算hashCode,然后在hashCode()方法中,直接返回hashCode。
对于后者,代码可以写成这样,
private int hashCode = -1;
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int hashCode() {
if (hashCode() == -1) {
/** *此处计算hashCode */
}
return hashCode();
}
这样就可以保证hashcode只计算一次,防止多次调用hashcode带来的大量的计算。