Java中HashMap的实现原理

一、Java中的hashCode和equals

1、关于hashCode

  1. hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的
  2. 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同
  3. 如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点
  4. 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里“

再归纳一下就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。

以下对hashCode的解读摘自其他博客:

1.hashcode是用来查找的,如果你学过数据结构就应该知道,在查找和排序这一章有
例如内存中有这样的位置
0  1  2  3  4  5  6  7 
而我有个类,
这个类有个字段叫ID,我要把这个类存放在以上8个位置之一:

(1)如果不用hashcode而任意存放:
那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。

但如果用hashcode那就会使效率提高很多:

(2)我们这个类中有个字段叫ID,那么我们就定义我们的hashcode为ID%8,然后把我们的类存放在取得得余数那个位置。
比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。
这样,以后在查找该类时就可以通过ID除 8求余数直接找到存放的位置了。


2.但是如果两个类有相同的hashcode怎么办那(我们假设上面的类的ID不是唯一的),例如9除以8和17除以8的余数都是1,那么这是不是合法的,
回答是:可以这样。那么如何判断呢?在这个时候就需要定义 equals了。

也就是说,我们先通过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,
那么我们就需要再通过 equals 来在这个桶里找到我们要的类。

那么,重写了equals(),为什么还要重写hashCode()呢?

想想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不通过重写hashcode()来找到桶,光重写equals()有什么用啊

2、关于equals

1.equals和==
==用于比较引用和比较基本数据类型时具有不同的功能:
(1)比较基本数据类型,如果两个值相同,则结果为true
(2)而在比较引用时,如果引用指向内存中的同一对象,结果为true;

equals()作为方法,实现对象的比较。由于==运算符不允许我们进行覆盖,也就是说它限制了我们的表达。因此我们复写equals()方法,达到比较对象内容是否相同的目的。而这些通过==运算符是做不到的。

2.object类的equals()方法的比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:
java.io.file,java.util.Date,java.lang.string,包装类(Integer,Double等)
String s1=new String("abc");
String s2=new String("abc");
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
运行结果为false true

二、HashMap的实现原理

1.    HashMap概述

    HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

    在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

HashMap解决hash冲突的方法:

HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。源码如下:

public V put(K key, V value) {  
        if (key == null)  
            return putForNullKey(value);  

	//hash值是通过hash(key.hashCode())来获取的
    //得到key的hashcode,同时再做一次hash操作
        int hash = hash(key.hashCode());  

	//当拿到一个hash值,通过indexFor(hash, table.length)获取数组下标,
        int i = indexFor(hash, table.length);  

	//先查询是否存在该hash值,若不存在,则直接以Entry<V,V>的方式存放在数组中(即直接执行下面的代码:addEntry(hash, key, value, i);)
	//若存在:系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 
	//中(最后放进来的在链表最前方,最先前的在链表最底部,设计者认为后进来的数据,查询的可能性更高一些,所以设计的这种顺序),),  
        //那系统必须循环到最后才能找到该元素。  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;  
            //判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                V oldValue = e.value;  
                e.value = value;  
                return oldValue;  
            }  
        }  
        modCount++;  

	//2中情况:
	//如果上面的条件判断结果是:存在相同的hashcode,但key不相同,这时就是产生了hash冲突。  
        //Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。  
	
	//如果上面的条件判断结果是:不存在相同的hashcode 则直接以Entry<V,V>的方式存放在数组中
        addEntry(hash, key, value, i);  
        return null;  
    }

hash值冲突是发生在put()时,从源码可以看出,

1:hash值是通过hash(key.hashCode())来获取的,当put的元素越来越多时,难免或出现不同的key产生相同的hash值问题,也即是hash冲突。

2:当拿到一个hash值,通过indexFor(hash, table.length)获取数组下标,

(1)先查询是否存在该hash值,

(2)若不存在,则直接以Entry<V,V>的方式存放在数组中,

(3)若存在,则再对比key是否相同,若hash值和key都相同,则替换value,

(4)若hash值相同,key不相同,则形成一个单链表,将hash值相同,key不同的元素以Entry<V,V>的方式存放在链表中,这样就解决了hash冲突(最后放进来的在链表最前方,最先前的在链表最底部,设计者认为后进来的数据,查询的可能性更高一些,所以设计的这种顺序)这种方法叫做分离链表法,与之类似的方法还有一种叫做 开放定址法,开放定址法师采用线性探测(从相同hash值开始,继续寻找下一个可用的槽位)hashMap是数组,长度虽然可以扩大,但用线性探测法去查询槽位查不到时怎么办?因此hashMap采用了分离链表法。

 

总结:HashMap的实现原理:

  1. 利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
  3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
  4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页