1、概述
我们都知道HashMap
是一个非常常用的数据结构,在java8
之前是有数组+链表
组合构成,在java8
及之后都用的是数组+链表+红黑树
组合构成。数组的每个地方都存储了kv键值对
即Key-Value
,这些键值对在java7
中叫Entry
,在java8
中叫Node
。
其实HashMap
的底层原理就是哈希表,我么都知道哈希表是通过哈希函数计算元素应该放在数组的哪个位置,如果有两个元素通过哈希函数计算出来的值相同就会产生哈希冲突,而链表以及红黑树就是我们来解决哈希冲突的方法。我们通过HashMap
的源码来了解一下HashMap
的底层实现原理。
2、成员变量
//默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树转链表阈值
static final int UNTREEIFY_THRESHOLD = 6;
//链表转红黑树是最小的数组长度
static final int MIN_TREEIFY_CAPACITY = 64;
//多有node节点
transient Node<K,V>[] table;
//所有的键值对
transient Set<Map.Entry<K,V>> entrySet;
//元素的个数
transient int size;
//操作次数,fail-fast机制
transient int modCount;
//扩容阈值
int threshold;
//负载因子
final float loadFactor;
//链表节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
....
}
//红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
...
}
3、构造方法
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;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
//给定容量
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
//无参构造,将默认负载因子0.75赋值给负载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(Map<? extends K, ? extends V> m) {
//默认负载因子,将给定的map的值复制到现在的map中
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);//这个就是先根据m.size以及负载因子确定容量,然后扩容,遍历赋值
}
//tableSizeFor方法就是获取大于cap的最小2的n次方,至于为什么我们往后看
static final int tableSizeFor(int cap) {
//如果cap为14即1110
int n = cap - 1;//n为13即1101
n |= n >>> 1;//1101 >>> 1 = 0110,1101 | 0110 = 1111 = 15
n |= n >>> 2;//1111 >>> 2 = 0111, 1111 | 0111 = 1111 = 15
n |= n >>> 4;//...
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//判断是否容量超过最大容量
}
在这里面我们其实可以发现,除了用map
构造,其他构造方法只是给负载因子loadFactor
、threshold
扩容阈值也可以理解为容量。没有进行任何初始化操作。HashMap
把初始化操作放在put
方法中,也就是第一次put
的时候进行初始化操作,给table
等内容赋值。注意一点:我们在无参构造的时候只是给loadFactor
赋初值,而threshold
没有复制,有参构造的时候两个全部复制了,这点区别我们放在put方法中研究。
4、原理解析
4.1、hash值计算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//这是getNode方法中的部分代码
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
我们可以看到如果key
为空,计算出来的hash
值为0,说明HashMap
的key
值允许为空。
正常情况我们计算hash
值直接获取key
对象的hashCode
就可以了,为什么还要与hashCode
的高16位进行异或运算?
首先我们看getNode
方法中给定一个key
值,HashMap
是通过(n-1) & hash(key)
来确定的,为什么用这个式子嘞:
-
n
即table
长度也就是2的n次幂,我们看下面一些例子14 = 00001100 18 = 00010010 24 = 00011000 16 - 1 = 15 = 00001111 14 & 15 = 00001100 = 14 = 14 % 16 18 & 15 = 00000010 = 2 = 18 % 16 24 & 15 = 00001000 = 8 = 24 % 16
-
我们可以看出
(n - 1) & hash(key)
即求hash(key)
对n
进行取模运算。那我们再看这样一些运算hash1 = 10110010 00000000 10011101 11100011; hash1 >>> 16 = 00000000 00000000 10110010 00000000; hash1 ^ (hash1 >>> 16) = 10110010 00000000 00101111 11100011 = h1; hash2 = 11111010 00000000 10011101 11100011; hash2 >>> 16 = 00000000 00000000 11111010 00000000; hash2 ^ (hash2 >>> 16) = 11111010 00000000 01100111 11100011 = h2; n - 1 = 00000000 00000000 11111111 11111111; hash1 & (n - 1) = 00000000 00000000 10011101 11100011; h1 & (n - 1) = 00000000 00000000 00101111 11100011; hash2 & (n - 1) = 00000000 00000000 10011101 11100011; h2 & (n - 1) = 00000000 00000000 01100111 11100011;
-
我们看上述运算,hash1与hash2的低16位完全相同,不同的是高16位。此时n = 2 ^ 16,这是如果直接对hash1与hash2进行取模运算,那么结果一定是相同的,在哈希表中相同就代表发生了哈希冲突。看到这里详细大家就知道了为什么要与高16位进行异或运算了,就是为了降低发生哈希冲突的概率。
-
那么问题又来了,为什么要用异或运算而不是与、或、非运算?
1010 ^ 1100 = 0110 1010 & 1100 = 1000 1010 | 1100 = 1110
-
大家是不是有那么一丢丢的想法了,那就是异或的0、1个数相比于与、或运算接近于1 : 1。至于非运算?别开玩笑了,你能对两个对象使用非运算?
4.2、get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断数组是否为空、长度是否为零、该key对应的位置是否为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//是否当前位置的头结点key与hash值与给定的一致
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//判断当前位置是否只有一个元素
if ((e = first.next) != null) {
//判断是否为红黑树结构
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//到这里说明为链表结构且当头结点不是所需要的,长度不为一
do {
//是否当前位置的头结点key与hash值与给定的一致
if (e.hash == hash &&
((k &#