从源码分析java集合类原理(3)-HashMap原理分析(jdk1.7)

HashMap的原理在任何java面试中可以毫不夸张的说是被问到几率是最高的,很多拥有四五年工作经验的“老油条”可能也不能说明白其底层实现原理,今天我们就来把这个用的很多但是了解的很少的HashMap彻头彻尾的解析一遍。

在了解HashMap之前,首先我们要了解以下几个知识点

  1. 什么是Hash表?
  2. 什么是Hash算法?
  3. 什么是Hash冲突及Hash冲突的解决办法?

针对上面三个问题,是我们在了解HashMap原理之前必须了解的,它们是实现HashMap的基础。Hash表是一种数据结构,Hash表本质上还是一个数组,是一种特殊的线性表,它是通过Hash算法来决定数据在数组的存放位置的。举例说明Hash表的存取过程,Hash函数我们采用最简单的f(k)=k%4,数据集{5,6,7,8}

上面是简单的Hash表的存取过程,上面其实存在着一个问题就是如果我现在需要再向Hash表中添加一个数据9,你会发现通过Hash函数计算应该存放到下标为1的位置,但是现在下标为1的位置已经存在数据了,此时引出一个Hash表中的另一个概念-Hash冲突,也就是两个不同元素映射到同一个位置上的情况,这种情况肯定是不允许存在的,下面再继续讨论下几种常用的解决Hash冲突的解决办法:

(1)、链地址法。链地址法的基本思路是将出现Hash冲突的数据放在一个链表中,这样数组的每一个数据元素都可能是一个链表,而链表是一个可动态增长的线性表,这样从某种程度上可以解决Hash冲突的问题。继续用上面讲到例子,继续添加数据9之后的结构就变成下面的结构。

(2)、线性探测法。又称开放地址法,基本思路是在出现冲突的位置继续寻找下一个位置,如果下一个已经存在数据则继续寻找下一个位置,直到冲突解决。

(3)、再哈希法。再哈希法的基本思路就是同时定义多个Hash函数,如果出现冲突则换另一个Hash函数继续进行Hash运算,直到冲突解决。


介绍完Hash表和Hash函数及Hash冲突的相关特性之后,接下来正式进入文章的主题--HashMap的原理分析。这里注意jdk1.7和jdk1.8对HashMap的处理是有区别的,此篇文章我们先以jdk1.7 HashMap的原理进行分析。

在jdk1.7中HashMap采用数组加单链表的结构来存储数据,数据采用<key,value>的形式存储。HashMap的一个数据节点叫做Entry,其定义包括四个部分key,value,next,hash,其中key和value就是我们需要存储的数据,next是链表指向下一个节点的指针,hash是根据Hash函数对key进行hash运算得到的hash值,其源码定义如下:

另外在了解HashMap的原理之前我们首先需要了解几个概念:capacity容量是指HashMap中数组的大小(注意不是能够存储数据量大小,而是数组大小);loadFactor加载因子,随着HashMap数据量的不断增加,Hash冲突的概率会越来越大,添加和查找数据的成本会越来越高,为了解决这个问题,我们需要对HashMap进行扩容,而扩容的时机就是这个加载因子决定的,当HashMap的大小达到capacity*loadFactor的时候,HashMap会进行扩容,HashMap的默认容量是16,默认加载因子为0.75,threshold 就是Capacity*loadFactor的值,也就是HashMap的扩容临界值大小。

HashMap共有四个构造函数HashMap(int initialCapacity, float loadFactor),HashMap(int initialCapacity),HashMap(),HashMap(Map<? extends K, ? extends V> m)。

(1)、HashMap()。该构造函数默认容量为16,默认加载因子为0.75。

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

 (2)、HashMap(int initialCapacity)。该构造函数指定一个初始容量并使用默认加载因子0.75。

public HashMap(int initialCapacity) {
 this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

(3)、HashMap(int initialCapacity, float loadFactor)。该构造函数我们可以指定HashMap的初始容量跟加载因子。

public HashMap(int initialCapacity, float loadFactor) {
 if (initialCapacity < 0)
 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
 throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
    this.loadFactor = loadFactor;
 threshold = initialCapacity;
 init();
}

 (4)、HashMap(Map<? extends K, ? extends V> m)。该构造函数根据我们指定的Map对象来构造一个HashMap对象。

public HashMap(Map<? extends K, ? extends V> m) {
 this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
 DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
 inflateTable(threshold);

 putAllForCreate(m);
}

熟悉完HashMap中的一些简单概念及构造方法之后,我们继续通过源码来深入了解HashMap的数据存取操作的原理。

(1)、添加数据。向HashMap中添加数据是一个相对比较复杂的过程,如果是第一次添加数据,会调用inflateTable(int toSize),此方法的主要作用就是创建一个大小为capacity的数组。如果key为null则调用putForNullKey(V value)方法添加数据,否则通过hash()函数计算key值对应的hash值,然后根据hash值确定数据在数组的位置下标i,最后遍历数组i下标的链表,如果存在key值相同的节点,则直接替换value并返回旧值;如果不存在key值相同的节点,则通过addEntry方法向链表中添加新数据节点(新数据节点添加到链表的首位置)。

public V put(K key, V value) {
//如果table是空,也就是第一次put数据的时候,通过inflateTable初始化
 if (table == EMPTY_TABLE) {
        inflateTable(threshold);
 }
 if (key == null)
//添加key为空的数据
 return putForNullKey(value);
//根据hash函数计算key的hash值并根据hash值确定在数组中的位置
    int hash = hash(key);
    int i = indexFor(hash, table.length);
//遍历i位置上链表的链表,如果存在key值相同的节点,则更新节点的value值
    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++;
//如果链表中不存在key值相同的节点,则添加新节点
 addEntry(hash, key, value, i);
    return null;
}

 inflateTable初始化方法首先通过roundUpToPowerOf2方法返回一个大于toSize的2的幂值作为创建数组的大小。

private void inflateTable(int toSize) {
 // 返回大于toSize的2幂值,也就是当前HashMap的容量
 int capacity = roundUpToPowerOf2(toSize);

 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//新建一个capacity大小的数组 
table = new Entry[capacity];
 initHashSeedAsNeeded(capacity);
}

addEntry方法首先判断HashMap数据量是否达到扩容临界值并且数据存放数组的位置已经存在数据,此时会调resize方法进行扩容,扩容大小为原容量的2倍(size>threshold不一定会触发扩容机制,扩容的条件是两个,一个是size>threshold;一个是数组对应位置非空即已经存在数据,只有满足两个条件的时候才会扩容)。

void addEntry(int hash, K key, V value, int bucketIndex) {
//添加数据之前首先判断是否需要扩容,如果当前数据量大于临界值threshold并且插入的位置已经有数据,则需要扩容,扩容之后的容量大小为之前的2倍
 if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
 hash = (null != key) ? hash(key) : 0;
 bucketIndex = indexFor(hash, table.length);
 }
   //将新数据添加到数组中
    createEntry(hash, key, value, bucketIndex);
}

 resize方法是HashMap比较核心的一个方法,也是在很多面试中面试官会经常问到关于HashMap扩容原理问题的出处,扩容之前会判断当前容量是否已经达到最大值MAXIMUM_CAPACITY,如果达到容量最大值就不再继续扩容了。接下来用扩容后的容量新创建一个数组,再创建新数组之后,通过transfer方法将原数组中的所有数据key重新进行hash计算并放到新数组对应位置。

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
 threshold = Integer.MAX_VALUE;
        return;
 }
//新建一个扩容后容量的数组
 Entry[] newTable = new Entry[newCapacity];
//通过遍历原数组中的所有数据并重新计算hash值,然后将数据添加到新数组中
 transfer(newTable, initHashSeedAsNeeded(newCapacity));
//新数组赋值为原数组对象,至此数组扩容正式完成
 table = newTable;
//更新新的临界值
 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

 createEntry方法主要是通过已确定的数组下标将新数据添加到链表当中去,并且是添加到链表表头的位置。

void createEntry(int hash, K key, V value, int bucketIndex) {
//新节点的next为原bucketIndex位置的数据,换句话说,也就是新节点添加到了bucketIndex链表的表头
    Entry<K,V> e = table[bucketIndex];
 table[bucketIndex] = new Entry<>(hash, key, value, e);
 size++;
}

(2)、根据key值获取数据。获取数据的操作结合着添加数据操作来看就比较容易理解,首先判断key值是否为null,如果为null则在数组第一个位置table[0](前面讲过key为null的数据放在了table[0]的位置)的位置,然后遍历table[0]位置的链表直到找到key值相等的节点。key值不为空的情况,首先根据key进行hash计算找到key值对应的数组下标,然后遍历链表找到key值相等的节点。

public V get(Object key) {
 if (key == null)
	//key为null时获取
 return getForNullKey();
	//key不为null时获取数据
 Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
 if (size == 0) {
 return null;
 }
//根据上面添加数据的逻辑,key为null的数据都放在数组的第一个位置,遍历数组第一个位置的链表,找到key值为空的数据返回
 for (Entry<K,V> e = table[0]; e != null; e = e.next) {
 if (e.key == null)
 return e.value;
 }
 return null;
}
final Entry<K,V> getEntry(Object key) {
 if (size == 0) {
 return null;
 }
//获取key值的hash值
 int hash = (key == null) ? 0 : hash(key);
//首先根据hash值获取数据的数组下标,然后遍历链表直到key值相等节点,返回节点value
 for (Entry<K,V> e = table[indexFor(hash, table.length)];
 e != null;
 e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
 return e;
 }
 return null;
}

(3)、删除数据。删除数据查找目标数据的逻辑与(2)一致,不在赘述,当遍历找到目标数据节点之后需要删除节点,如果目标节点是首节点,这直接将目标节点的后继节点赋值给数组对应位置。如果目标节点不是首节点,则将目标节点的后继节点作为目标节点前驱节点的后继节点(可自行学习了解一下链表的删除)。

final Entry<K,V> removeEntryForKey(Object key) {
 if (size == 0) {
 return null;
 }
	//首先通过Hash函数找到对应key值对应的数组位置
 int hash = (key == null) ? 0 : hash(key);
    int i = indexFor(hash, table.length);
 Entry<K,V> prev = table[i];
 Entry<K,V> e = prev;
//遍历数组位置链表,找到key值对应的数据节点,然后改变
    while (e != null) {
        Entry<K,V> next = e.next;
 Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
 modCount++;
 size--;
//如果是key值是首节点,直接将首节点的后继节点赋值给数组i位置
            if (prev == e)
 table[i] = next;
            else
//如果key不是首节点,则将目标节点的后继节点作为目标节点前驱节点的后继节点(链表的删除)
 prev.next = next;
 e.recordRemoval(this);
            return e;
 }
        prev = e;
 e = next;
 }

 return e;
}

总结:HashMap是通过数组加链表的形式进行数据存储的,当存储数据量达到Capacity*loadFactor并且数组对应位置已经存在数据时需要对其进行扩容,扩容的中心思想就是重新创建一个之前两倍的数组,并对原数组数据重新进行Hash计算并添加到新数组中。

才疏学浅,码字不易,有总结不到位的请各路大神多多指教,欢迎交流!

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java码仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值