为了账号安全,请及时绑定邮箱和手机立即绑定

JAVA为什么两个不同对象的hashCode有可能相同

标签:
Java

hashCode:

hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。public int hashCode()返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。

理解:

虽然Set同List都实现了Collection接口,但是他们的实现方式却大不一样。List基本上都是以Array为基础。但是Set则是在 HashMap的基础上来实现的,这个就是Set和List的根本区别。HashSet的存储方式是把HashMap中的Key作为Set的对应存储项。看看 HashSet的add(Object obj)方法的实现就可以一目了然了。

hashCode是所有Java对象的固有方法,如果不重载的话,返回的实际上是该对象在jvm的堆上的内存地址,而不同对象的内存地址肯定不同,所以这个hashCode也就肯定不同了。如果重载了的话,由于采用的算法的问题,有可能导致两个不同对象的hashCode相同。

而且,还需要注意一下两点:

1)hashCode和equals两个方法是有语义关联的,它们需要满足:

A.equals(B)==true --->  A.hashCode()==B.hashCode()

因此重载其中一个方法时也需要将另一个也重载。

2)hashCode的重载实现需要满足不变性,即一个object的hashCode不能前一会儿是1,过一会儿就变成2了。hashCode的重载实现最好依赖于对象中的final属性,从而在对象初始化构造后就不再变化。一方面是jvm便于代码优化,可以缓存这个hashCode;另一方面,在使用hashMap或hashSet的场景中,如果使用的key的hashCode会变化,将会导致bug,比如放进去时key.hashCode()=1,等到要取出来时key.hashCode()=2了,就会取不出来原先的数据。
哈希表是结合了直接寻址和链式寻址两种方式,所需要的就是将需要加入哈希表的数据首先计算哈希值,其实就是预先分个组,然后再将数据挂到分组后的链表后面,随着添加的数据越来越多,分组链上会挂接更多的数据,同一个分组链上的数据必定具有相同的哈希值,Java中的hash函数返回的是int类型的,也就是说,最多允许存在2^32个分组,也是有限的,所以出现相同的哈希码就不稀奇。

HashSet、HashMap

  1. HashSet和HashMap一直都是JDK中最常用的两个类,HashSet要求不能存储相同的对象、无序、线程非同步,底层是哈希表结构。但它是怎么做到的?什么是散列表数据结构(哈希表)?有什么特性?
  2. HashMap要求不能存储相同的键。
  3. 那么Java运行时环境是如何判断HashSet中相同对象、HashMap中相同键的呢?当存储了“相同的东西”之后Java运行时环境又将如何来维护呢?
    分析:
    在研究这些问题之前,首先说明一下JDK对equals(Object obj)和hashCode()这两个方法的定义和规范:
    在Java中任何一个对象都具备equals(Object obj)和hashcode()这两个方法,因为他们是在Object类中定义的。
    equals(Object obj)方法用来判断两个对象是否“相同”,如果“相同”则返回true,否则返回false。
    hashcode()方法返回一个int数,在Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。
    接下来有两个关于这两个方法的重要规范:
    规范1:若重写equals(Object obj)方法,有必要重写hashcode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashcode()返回值。说得简单点就是:“如果两个对象相同,那么他们的hashcode应该 相等”。不过请注意:这个只是规范,如果你非要写一个类让equals(Object obj)返回true而hashcode()返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了Java规范,程序也就埋下了BUG。
    规范2:如果equals(Object obj)返回false,即两个对象“不相同”,并不要求对这两个对象调用hashcode()方法得到两个不相同的数。说的简单点就是:“如果两个对象不相同,他们的hashcode可能相同”。
    根据这两个规范,可以得到如下推论:
    1、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
    2、如果两个对象不equals,他们的hashcode有可能相等。
    3、如果两个对象hashcode相等,他们不一定equals。
    4、如果两个对象hashcode不相等,他们一定不equals。

这样我们就可以推断Java运行时环境是怎样判断HashSet和HastMap中的两个对象相同或不同了。(先判断hashcode是否相等,再判断是否equals。 )

再来看HashSet的源码,首先看默认构造器:

[java] 
public HashSet() {
 map = new HashMap<E,Object>();
}
// 构造器中new了一个HashMap。key使用了泛型,value使用Object。

再来看add方法源码:

[java]
private static final Object PRESENT = new Object(); 
public boolean add(E e) { 
    return map.put(e, PRESENT)==null; 
} 
// PRESENT是一个Object类型的常量,用来当做map的value。也就是说,在HashSet中存储的元素都是HashMap中key,value全部使用Object。 

HashMap的key是不可以重复的,保证元素唯一的依据是对象的hashCode跟equals方法。
而HashSet不就是用HashMap的key来存储元素嘛,也就保证了元素的唯一性。包括迭代器也是HashMap中keySet方法取得的iterator。

[java] 
public Iterator<E> iterator() { 
    return map.keySet().iterator(); 
} 
public Iterator<E> iterator() {
 return map.keySet().iterator();
}

通过上面的介绍,已经对HashSet比较了解了,我们知道HashSet底层是用了HashMap。
要想知道怎么做到存取速度快的,我们直接看HashMap就好了。

散列表数据结构(哈希表)

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

HashMap底层就是散列表数据结构,即数组和链表的结合体,底层是一个数组结构,数组中存储的值又是一个链表。这样做有什么好处呢?
数组能够提供对元素的快速访问但不易于扩展(如果不知道元素脚标,还得进行遍历查找),链表易于扩展但不能对其元素进行快速访问。怎样做到两全其美,就是散列表数据结构。

我们来看看HashMap中元素存跟取的实现方式:

首先明白,HashMap根据key的hashCode计算出元素在Entry数组中的位置,然后再Entry内部链表中存放key,value。

我们来对HashMap存取元素的过程来做一个小的总结:

元素(key,value)在HashMap中被封装进Entry数组。
put元素的时候,根据key的hash值定位元素在Entry数组中的索引,如果当前索引有元素,就通过equals方法进行比较,将元素存进当前Entry的链表中适当的位置。
get元素的时候,根据key的hash值定位元素在Entry数组中的索引,然后通过equals方法定位元素在链表中的位置,取出该元素。

性能分析:

因为元素的存取是通过hash算法进行的,所以速度都没的说。
在查找操作中,唯一影响性能的是在链表中,但实际只要优化好了key对象hashCode跟equals方法,就会避免链表中的数据过多而导致查找性能变慢。

再一个非常影响性能的是数组扩容操作,当使用默认的DEFAULT_INITIAL_CAPACITY对HashMap进行初始化的时候,
如果元素个数非常多,会导致扩容次数增加,每次扩容都会进行元素位置的重新分配,这是相当耗费性能的。
如果能预算好元素个数,就应该避免使用默认的DEFAULT_INITIAL_CAPACITY,可在HashMap的构造函数中为其指定一个初始值。

问题解决:
hashCode必须和equals保持兼容(equals方法的判断依据和计算hashCode的依据相同),这样做是为了避免链表中的数据过多。

例:
首先我们定义一个类,重写hashCode()和equals(Object obj)方法:

class A {  
    @Override  
    public boolean equals(Object obj) {  
        System.out.println("判断equals");  
        return false;  
    }  
    @Override  
    public int hashCode() {  
        System.out.println("判断hashcode");  
        return 1;  
    }  
}  

然后写一个测试类,代码如下:

public class Test {    
    public static void main(String[] args) {    
        Map<A,Object> map = new HashMap<A, Object>();    
        map.put(new A(), new Object());    
        map.put(new A(), new Object());        
        System.out.println(map.size());    
    }       
}    

运行之后打印结果是:

判断hashcode 
判断hashcode 
判断equals 
2 

可以看出,Java运行时环境会调用new A()这个对象的hashcode()方法。其中:
打印出的第一行“判断hashcode”是第一次map.put(new A(), new Object())所打印出的。
接下来的“判断hashcode”和“判断equals”是第二次map.put(new A(), new Object())所打印出来的。

那么为什么会是这样一个打印结果呢?
1、当第一次map.put(new A(), new Object())的时候,Java运行时环境就会判断这个map里面有没有和现在添加的 new A()对象相同的键,判断方法:调用new A()对象的hashcode()方法,判断map中当前是不是存在和new A()对象相同的HashCode。显然,这时候没有相同的,因为这个map中都还没有东西。所以这时候hashcode不相等,则没有必要再调用 equals(Object obj)方法了。参见推论4(如果两个对象hashcode不相等,他们一定不equals)
2、当第二次map.put(new A(), new Object())的时候,Java运行时环境再次判断,这时候发现了map中有两个相同的hashcode(因为我重写了A类的hashcode()方 法永远都返回1),所以有必要调用equals(Object obj)方法进行判断了。参见推论3(如果两个对象hashcode相等,他们不一定equals),然后发现两个对象不equals(因为我重写了equals(Object obj)方法,永远都返回false)。
3、这时候判断结束,判断结果:两次存入的对象不是相同的对象。所以最后打印map的长度的时候显示结果是:2。

改写程序如下:

class A {  
    @Override  
    public boolean equals(Object obj) {  
        System.out.println("判断equals");  
        return true;  
    }  
    @Override  
    public int hashCode() {  
        System.out.println("判断hashcode");  
        return 1;  
    }  
}  
public class Test { 
    public static void main(String[] args) {  
        Map<A,Object> map = new HashMap<A, Object>();  
        map.put(new A(), new Object());  
        map.put(new A(), new Object());   
        system.out.println(map.size());  
    }   
} 

运行之后打印结果是:

判断hashcode 
判断hashcode 
判断equals 
1 

显然这时候map的长度已经变成1了,因为Java运行时环境认为存入了两个相同的对象。原因可根据上述分析方式进行分析。

以上分析的是HashMap,其实HashSet的底层本身就是通过HashMap来实现的,所以它的判断原理和HashMap是一样的,也是先判断hashcode再判断equals。

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
37
获赞与收藏
353

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消