目录
一、HashMap底层用到了哪些数据结构?
HashMap用到了数组,链表,红黑树。 数组中的每一个元素都是一个Key-Value键值对结构的Node对象/TreeNode对象,数组初始长度为16。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
/** 16 初始容量
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** 最大容量
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/** 扩容因子,元素个数达到容量的75%开始扩容
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** 链表节点数达到8,转换为红黑树
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/** 红黑树节点个数小于6转为链表
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
...省略...
//一个 Node 代表一个key-value 键值对,也是数组中的一个元素
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//key的hash值
final K key; //键
V value; //值
Node<K,V> next; //指向下一个Node
}
结构图:
二、put方法流程
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2.1、hash(key)方法讲解:首先判断key是否为null,如果为null,则返回0;如果不为null,则将获取key的hashCode和h>>>16进行位异或运算,将算出来的结果返回。可以得出个结论:key/键可以为null值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.2、然后开始讲putVal方法,一点一点讲解:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//😊1.如果数组是空的,调用 resize() 初始化数组
if ((tab = table) == null || (n = tab.length) == 0)
//😊tab是数组,n为数组的长度
n = (tab = resize()).length;
//😊2.计算位置: (n - 1) & hash 等同于 hash % 16 ,计算key的存储位置
//😊3.取出该位置的元素是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
//😊如果为空,就创建一个新Node,把当前的key-value存储进去
tab[i] = newNode(hash, key, value, null);
else {
//😊到这里说明hash冲突了,该位置有元素,那么就有2种执行方案:
//😊如果有和当前传入key相同的key存住,覆盖值,如果没有和传入的key相同的key,那就添加元素
Node<K,V> e; K k;
//😊5.如果key相等,hash值也相等,进行值的覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//😊6.如果不满足第5步,那就要进行链表或者红黑树的遍历了 , 这里在判断是不是红黑树
else if (p instanceof TreeNode)
//😊走红黑树的添加流程
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//😊7.到这里就是链表了
//😊8.通过next一个一个往下遍历,如果某一个node的next为空 ,说明找到最后一个Node了,
//就把新的key-value加入最后一个元素的next
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//😊这里在判断是否要做红黑树转变
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//😊9.如果在遍历的过程中,找到了和传入的key一样的key,hash值也一样,那就进行值的覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
//😊10.用新的值覆盖老的值,然后将旧值返回去
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
总结一下:首先判断table是否为空,如果为空,则初始化table。如果不为空则进行添加元素,先通过(n-1)&hash计算出在数组的索引值,然后判断该索引是否有数据,如果没有则直接创建一个Node存到该索引上,如果有则说明该索引上已有数据(称为hash冲突),那么就有两种情况:第一种情况是当该索引上的Node的key是否与我们传进来的key相同并且hash相同,如果相同,则覆盖value;如果不相同,则就是第二种情况,先判断是否是红黑树,如果是,则走红黑树的添加流程;如果不是,则走链表的添加流程,通过循环找到next为null的Node,当在循环的过程中,如果找到了和传入的key一样的key,hash值也一样,那么就覆盖value;如果没有,则将传进来的hash、key、value封装到成一个Node对象,使next为null的Node指向该新建的Node,然后进行判断是否需要转换为红黑树。
三、get方法流程
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
3.1、如果getNode能get到对应key的值就返回对应的值,如果没有,则返回null值。可以得出一个结论:value可以为null
3.2、我们来看看它是怎么getNode的
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
//😊 1.首先判断数组是否为空,如果为空,则直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
//😊计算key存储位置,取出该位置的元素,判断该索引上是否有数据,如果无元素则直接返回null
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
//😊2.检查是不是第一个元素,比较key,如果是直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//😊直接返回第一个元素
return first;
if ((e = first.next) != null) {
//😊3.如果是红黑树,就遍历红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//😊一直next遍历下一个元素,然后比较key,知道遍历完
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//经过以上操作都没有get到,那么就返回null
return null;
}
总结一下:首先判断数组是否为空,如果为空,则直接返回null;如果不为空,则进行寻找。通过(n - 1) & (hash = hash(key))计算在数组的索引,然后判断第一个元素是否是该元素,如果是就直接返回元素;如果不是,那就再继续寻找。先判断该索引上是红黑树还是链表,如果是红黑树,则遍历红黑树进行寻找元素;如果是链表,则遍历链表进行寻找元素。
四、HashMap为什么要用到链表结构?
使用链表是为了解决Hash冲突问题,这就要说到HashMap存储元素的位置映射算法了,HashMap通过 hash(key) % 16
算法来计算某个key在数组中的存放位置 ,通过hash算法得到 key的hash值,对数组长度(16)取余数,结果为0到15其中一个数字,正好对应数组的索引位置。
而Hash冲突就是两个不同的 key ,使用Hash函数得到了相同的Hash值(这是有可能的),这就意味着两个各不同键值对计算出了相同的存储位置(一个位置怎么能放两个键值对呢?)这就是Hash冲突。
HashMap使用 链地址法 来解决Hash冲突,其实就是把相同位置的元素以链表的方式进行存储,如下:
你现在应该知道Node节点对象中的 next 的作用了。
五、 HashMap为什么要用到红黑树?
HashMap使用红黑树是为了解决链表过长,查询变慢的问题
大家都知道红黑树的查询性能是及其可观的。
如果HashMap出现Hash冲突的情况变多,那么链表的长度会越来越长,由于链表的查询方式是从前往后一个一个遍历,查询速度比较慢,所以当链表长度达到 8 的时候 ,HashMap会把链表变成一个红黑树结构来提高查询效率 , 当删除元素,红黑树的节点数小于6又会把红黑树变回链表 , 下面这2个字段就是红黑树和链表的转换阈值:
/** 链表节点数达到8,转换为红黑树
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/** 红黑树节点个数小于6转为链表
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
六、HashMap如何扩容的?
HashMap的扩容因子是0.75,也就是说当数组中的已存储节点数(Node) > 数组容量 ∗ 扩容因子时
,就需要扩容,调整数组的大小为当前的 2 倍
/** 扩容因子,元素个数达到容量的75%开始扩容
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
七、HashMap是线程不安全的
HashMap暴露给外界的方法并不是同步的,在多线程环境中是可能会出现线程安全问题,在多线程环境中可以使用HashTable ,或者ConcurrentHashMap。 HashTable直接使用 synchronized 同步锁来保证线程安全,比较古老现在基本不用了。ConcurrentHashMap是JUC并发库中的容器 ,它是专门为多线程环境设计,并发控制使用 Synchronized 和 CAS 来操作,性能良好,在JDK1.8中ConcurrentHashMap也使用到了数组链表红黑树的结构来存储数据。
八、HashMap和HashTable的区别
HashTable是HashMap的线程安全版本,也是比较老旧的一个map,HashTable方法是加了同步锁,线程安全,性能会受到影响。而HashMap没有加同步锁,线程不安全,但是性能更高。
Hashtable的key和value都不支持null , 而HashMap值可以为null,key支持一个key为nul。