【Java】hashCode方法 与 hashMap

来源:互联网 发布:查看oracle监听端口 编辑:程序博客网 时间:2024/06/15 12:49

Object类有一些方法,我们定义的类型都会继承自Obejct类,所以它的方法我们可能会改写,比如常见的toString,equals和hashCode方法。但是需要注意,这些方法在Java规范中都有一些约定,我们在覆盖时需要小心,要遵守这些约定,否则会导致程序中的错误或者意料之外的结果。下面是hashCode方法覆盖要注意的问题。

首先要明白hashCode方法是干嘛的。在集合类型中,我们如果要查找一个元素,通常是遍历,调用equals方法比较,若相等返回。但是有一个数据结构叫map它可以避免遍历,实现O1随机存储,map的实现靠的就是hashCode方法。放入一个元素先计算其hashCode方法,这个hashCode方法可以返回一个int值,以它为参数计算hash值,这个值就是槽位,有一个链表,hash值相同的元素都在这个槽位的链表中。那么查找时,我们不需要遍历全部的元素调用equals,只需要对某一个槽位里面的元素调用equals方法即可,极大地减少了调用equals方法的次数,这就是map结构的原理。当然,map并不是依赖于java平台的,它是一个通用的数据结构,与语言无关,只是java中的hashCode方法,使得map这个结构在java中的实现变得很方便。这就是hashCode方法存在的原因。

那么我们如果自定义了一个用户类,它的hashCode方法如何处理?

如果我们不覆盖,那么jvm默认实现是通过该对象的内存地址来计算code的。

如果我们要覆盖呢?那么,需要满足下面三个条件:

(1)对同一个对象多次调用,应该返回相同的code;

(2)两个对象的equals方法相等,则hashCode返回也必须相等;

(3)两个对象的equals方法不相等,则hashCode返回不一定相等;

第一条显然需要支持,因为如果不等,放入map时计算出了一个槽位,取出时计算的槽位是另一个,那么就无法得到这个元素了。第二条涉及了euals方法,原则上,对于对象,如果equals方法返回的值相同,我们认为是同一个对象,更具体得,指的是这两个引用指向了相同的对象。那么在map中,作为key的对象有什么含义呢?我觉得就是一个标识,标签。那么如果指向相同的对象,这两个key就会被map识别为同一个标识,这是显然的。除此之外呢?如果两个对象的属性完全一样呢?假设我们自定义了一个复数类,new了两个对象,他们的值完全一样,即实部和虚部一样,虽然这两个引用指向了不同的对象,但是逻辑上,我们认为他们相等,这样的类型可以叫做“值类”。对于“值类”我们往往需要重写equals方法,以表达刚才的含义。这样,虽然不是同一个对象,但是他们在作为map的key时所表现出的标识的含义必须相同,即他们对应的元素value要放入相同的槽位。这个时候我们不重写hashCode函数,就无法保证两个对象在equals返回true时对应同一个槽位。所以此时是违反第二条的,我们在重写equals时也要重写hashCode,满足条件二。

对于条件三,事实上,我们更追求“两个对象的equals方法不相等,则hashCode返回尽可能地不相等”,因为这样保证了每一个槽位里面的链表数目尽可能少,equals方法次调用次数也少,冲突少。


例子,定义一个虚数类,有实部和虚部。

不重写任何方法,那么默认equals调用==,hashCode返回基于地址的int值。

import java.util.HashMap;import java.util.List;import java.util.Map;class Number{ int shi; int xu;  public Number(int shi, int xu){ this.shi = shi; this.xu = xu; }}public class Case2 {public static void main(String[] args) {Map<Number, String> map = new HashMap<>();Number n1 = new Number(10, 10);Number n2 = new Number(10, 10);map.put(n1, "123");map.put(n2, "456");System.out.println("equals : " + n1.equals(n2));System.out.println("hash : " + n1.hashCode());System.out.println("hash : " + n2.hashCode());System.out.println(map.get(n1));System.out.println(map.get(n1));System.out.println(map.get(n2));}}

可以看到,因为hashCode不同,因此放入了不同的槽位。但是这么说也不完全对,因为他们可能恰巧对应了相同的槽位,但是概率极低。总之在这个例子里面,hash值不同,是基于地址算出来的。

那么这个Number类作为key更理想的结果应该是,只要他们值一样,就应该认为是相同的,虽然可能是不同的对象。此时我们为了应对这种需求,首先要重写equals方法,不再是简单执行==,而要写基于虚部实部的判断。

在Number类添加方法:

 @Override public boolean equals(Object n){ Number nn = (Number)n; return ((this.shi == nn.shi) && (this.xu == nn.xu)); }
这时只要实部虚部相等number就相同,那么把它作为key,实部虚部相同就应该是同一个key,也就是说n2的value应该覆盖n1.但是结果是:

没有覆盖,那是因为没有修改hashCode,违法了之前的条件二,equals返回true,就是相等,那么就要对应相同的hash表的槽位。所以hashCode方法也要重写。

可以重写为:

 @Override public int hashCode(){ int r = 0; return r; }
那么会放到同一个槽,但是这样碰撞每一次都会发生,退化为了链表,所以就像条件三,equals不同,我们应该尽量让hashCode不同。所以计算hash值需要用到equals方法中用到的值,同时计算时还要考虑顺序。比方把每一个属性计算hash相加是不行的,这样最起码顺序不同的值最后的hash值也是相同的。在Java effective一书中推荐了一个算法。初始化r=0,然后计算每一个属性的hash值,让r加上hahs值乘以31,直到遍历所有的属性。用31是因为31乘法计算可以简化为移位运算和减法,jvm都会优化,效率高。

 @Override public int hashCode(){ int r = 0; r = 31 * r +  shi; r = 31 * r + xu; return r; }
最后结果是:



这次n2对应的value覆盖了n1的,说明在同一个槽位。

这是在重写equals方法之后需要注意的,要注意重写hashCode,使得在map中不会产生错误。


关于自定义类在hashMap中作为key而言,还要注意key的不应该允许改变,因为一旦改变,那么hashCode可能就会不同,导致之前插入的数据取不到。

比如:

import java.util.HashMap;import java.util.List;import java.util.Map;class Number{ int shi; int xu;  public Number(int shi, int xu){ this.shi = shi; this.xu = xu; }  @Override public boolean equals(Object n){ Number nn = (Number)n; return ((this.shi == nn.shi) && (this.xu == nn.xu)); }  @Override public int hashCode(){ int r = 0; r = 31 * r +  shi; r = 31 * r + xu; return r; }}public class Case2 {public static void main(String[] args) {Map<Number, String> map = new HashMap<>();Number n1 = new Number(10, 10);Number n2 = new Number(10, 10);map.put(n1, "123");map.put(n2, "456");System.out.println("equals : " + n1.equals(n2));System.out.println("hash : " + n1.hashCode());System.out.println("hash : " + n2.hashCode());System.out.println(map.get(n1));n2.xu = 10000;System.out.println(map.get(n2));}}
先把n2作为键插入map,然后把n2的属性改了,最后取,发现是null,所以key不能变。


以集合类型作为key也是相同的问题。

Map<List<Integer>, String> map = new HashMap<>();List<Integer> l1 = new ArrayList<>();l1.add(1);l1.add(2);map.put(l1, "123");System.out.println(map.get(l1));l1.add(3);map.get(l1);System.out.println(map.get(l1));
结果:



原创粉丝点击