Java 集合系列07--- HashMap详细介绍(源码解析)----新

Posted 飞飞1024

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合系列07--- HashMap详细介绍(源码解析)----新相关的知识,希望对你有一定的参考价值。

前言

今天学习了基于JDK1.8的HashMap的源码,主要从如下几个方面来阐述,HashMap的数据结构,HashMap如何支持动态扩容,HashMap的散列函数是如何实现的,并且如何防止散列冲突,最后就是对HashMap的常用方法的源码解析。

目录

  1. HashMap的数据结构
  2. HashMap的散列函数
  3. 散列冲突的处理
  4. HashMap的扩容机制
  5. put 方法的源码解析
  6. get 方法和remove的源码解析

基本的全局常量

  1. 默认初始化的容器大小16:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  // aka 16  1 左移4位
  1. 最大的数据容量2的30次方。也就是说最多存放2的30次方个数据
static final int MAXIMUM_CAPACITY = 1 << 30;
  1. 默认的加载因子 0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;

PS: 散列表的加载因子=填入表中的元素个数/散列表的长度
加载因子越大,说明空闲位置越小,冲突越多,散列表的性能会下降。
4. 默认的链表转红黑树的链表长度

static final int TREEIFY_THRESHOLD = 8;
  1. 默认的红黑树转链表的红黑树节点个数
static final int UNTREEIFY_THRESHOLD = 6;
  1. 最小的链表树形化的table的大小。
static final int MIN_TREEIFY_CAPACITY = 64;

HashMap的数据结构(基于JDK1.8)

HashMap的数据结构是散列表+链表+红黑树,其中散列表是其基本的数据结构(散列表使用的是桶数组,其实就是一个容量为N的普通数组A[0…N-1]。只不过,在这里我们要将每个单元都想象成一个"桶"(Bucket),每个桶单元里都可以存放一个条目。)。链表是用来存储散列值相同的结点的,当链表的默认长度大于8时链表就可能会转化成红黑树。
其数据结构图如下图所示:
Java
从源码可知,HashMap类中有个非常重要的字段​​​Node<K,V>[] table​​,即哈希桶数组,其实本质上就是一个数组。而Node是HashMap的一个内部类 ,实现了Map.Entry<K,V>接口,本质上就是一个键值对。

static class Node<K,V> implements Map.Entry<K,V> 
//用来定位数组索引位置(hash值)
final int hash;
//hash表的键
final K key;
//存储的值
V value;
//链表的下一个结点
Node<K,V> next;

HashMap的散列函数

散列表中,我们需要一个函数,将任意键key转换为介于0与N-1之间的整数,这个函数就是散列函数(又称哈希函数),散列函数应该要满足如下三点基本要求:

  1. 散列函数计算得到的散列值必须是一个非负整数(因为数组的下标不可能是负数)
  2. 如果key1=key2, 那么hash(key1)=hash(key2)。
  3. 如果key1=/=key2, 那么hash(key1)=/=hash(key2)。
    在HashMap中散列函数的实现如下:
static final int hash(Object key) 
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

如上代码我们可以看出hashMap的散列函数是通过调用key的hashCode()方法得到其hashCode值(该方法适用于所有Java对象)。然后再通过hashCode值的高16位异或低16位(其中h >>> 16表示在二进制中将h右移16位)来得到hash值。
最后通过 ​​​(n - 1) & hash;​​​(n-1对hash值做按位与运算,也就是求模运算) 得到该键值对的存储位置 。
下面举例说明,n为table的长度
Java

散列冲突的处理

当两个key定位到相同的位置是,就会发生散列冲突,散列函数计算结果越分数均匀,散列冲突的概率就会越小,map存储的效率就会越高。在HashMap中是通过链表法来处理,即位置相同的结点会存储到同位置上的链表上。具体的代码实现如下:

//遍历链表
for (int binCount = 0; ; ++binCount)
//当p.next (后继指针)为null时,则设置node结点
if ((e = p.next) == null)
p.next = newNode(hash, key, value, null);

//如果键和值已经存在则返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;

如上代码所示:散列冲突之后

  1. 首先遍历链表,在循环中,当p.next (后继指针)为null时,则设置node结点。
  2. 如果键和值已经存在则直接返回已经存在的数据。

HasMap的扩容机制

如果哈希桶数组很大,即使较差的散列函数也会比较分散,如果哈希桶数组很小,即使再好的散列函数,也会出现较多的散列冲突。所以,我们需要权衡时间成本和空间成本上权衡。其实就是根据实际情况确定哈希桶数组大小。并在此基础上设计较好的散列函数,HashMap就是通过良好的散列函数加扩容机制来控制map使得Hash碰撞较小。介绍扩容机制之前,我们需要知道几个重要的属性

int threshold;  //map所能容纳的键值对的极限
final float loadFactor; //装载因子
int modCount; //记录HashMap被结构修改的次数,用于fast-fail
int size; //map中包含的key-value的个数

HashMap的构造器主要是给这几个属性设值。 正如前面说到的HashMap默认的容器大小(capacity)是16,默认的转载因子(loadFactor)是0.75,而 threshold=​​loadFactor*capacity​​​,也就是说转载因子越大,map所能容纳的键值对就越多。 当HashMap中元素的个数超过threshold就会启动扩容,每次扩容都会扩容到原来的两倍大小。默认的装载因子0.75是对空间和效率的一种平衡选择,建议大家不要修改。而size 表示HashMap中实际存在的键值对数量,modCount字段主要是用来记录HashMap内部结构发生变化的次数,主要用于迭代快速失败。例如put新键值对,但是对某个key对应的value值覆盖不属于结构变化。
其扩容主要分为如下两步:

  1. 创建一个新的两倍于原容量的数组。
  2. 循环将原数组中的数据移到新数组中。
    具体代码如下:
final Node<K,V>[] resize() 
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) //不是初始化,走扩容流程
//超过最大值就不在扩容
if (oldCap >= MAXIMUM_CAPACITY)
threshold = Integer.MAX_VALUE;
return oldTab;

//扩容后的容量是原来的2倍,左移一位就可以将数据翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; //向左位移一位,达到原阀值的2倍。

else if (oldThr > 0) // 初始化容量,容量大小是threshold
newCap = oldThr;
else // zero initial threshold signifies using defaults
//容量,阀值指定初值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

//计算新的resize上限
if (newThr == 0)
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);

threshold = newThr;
//建立新容量数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例

Java集合系列五HashMap解析

Java入门系列之集合HashMap源码分析(十四)

Java 集合系列11之 Hashtable详细介绍(源码解析)和使用示例

Java 集合系列11之 Hashtable详细介绍(源码解析)和使用示例

Java集合系列四HashSet和LinkedHashSet解析