HashMap 源码和底层原理在现在面试中是必问的。因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合。文章较长,介绍了很多原理性的问题,希望对你有所帮助~
目录本篇文章主要包括以下内容:
HashMap 的存储结构
常用变量说明,如加载因子等
HashMap 的四个构造函数
tableSizeFor()方法及作用
put()方法详解
hash()方法,以及避免哈希碰撞的原理
resize()扩容机制及原理
get()方法
为什么HashMap链表会形成死循环,JDK1.8做了哪些优化
正文说明:本篇主要以JDK1.8的源码来分析,顺带讲下和JDK1.7的一些区别。
HashMap存储结构这里需要区分一下,JDK1.7和 JDK1.8之后的 HashMap 存储结构。在JDK1.7及之前,是用数组加链表的方式存储的。
但是,众所周知,当链表的长度特别长的时候,查询效率将直线下降,查询的时间复杂度为 O(n)。因此,JDK1.8 把它设计为达到一个特定的阈值之后,就将链表转化为红黑树。
这里简单说下红黑树的特点:
每个节点只有两种颜色:红色或者黑色
根节点必须是黑色
每个叶子节点(NIL)都是黑色的空节点
从根节点到叶子节点,不能出现两个连续的红色节点
从任一节点出发,到它下边的子节点的路径包含的黑色节点数目都相同
由于红黑树,是一个自平衡的二叉搜索树,因此可以使查询的时间复杂度降为O(logn)。(红黑树不是本文重点,不了解的童鞋可自行查阅相关资料哈)
HashMap 结构示意图:
常用的变量在 HashMap源码中,比较重要的常用变量,主要有以下这些。还有两个内部类来表示普通链表的节点和红黑树节点。
//默认的初始化容量为16,必须是2的n次幂 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量为 2^30 static final int MAXIMUM_CAPACITY = 1 << 30; //默认的加载因子0.75,乘以数组容量得到的值,用来表示元素个数达到多少时,需要扩容。 //为什么设置 0.75 这个值呢,简单来说就是时间和空间的权衡。 //若小于0.75如0.5,则数组长度达到一半大小就需要扩容,空间使用率大大降低, //若大于0.75如0.8,则会增大hash冲突的概率,影响查询效率。 static final float DEFAULT_LOAD_FACTOR = 0.75f; //刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树 static final int TREEIFY_THRESHOLD = 8; //当红黑树上的元素个数,减少到6个时,就退化为链表 static final int UNTREEIFY_THRESHOLD = 6; //链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。 //这是为了避免,数组扩容和树化阈值之间的冲突。 static final int MIN_TREEIFY_CAPACITY = 64; //存放所有Node节点的数组 transient Node<K,V>[] table; //存放所有的键值对 transient Set<Map.Entry<K,V>> entrySet; //map中的实际键值对个数,即数组中元素个数 transient int size; //每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。 //当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。 transient int modCount; //数组扩容阈值 int threshold; //加载因子 final float loadFactor; //普通单向链表节点类 static class Node<K,V> implements Map.Entry<K,V> { //key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置 final int hash; final K key; V value; //指向单链表的下一个节点 Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } } //转化为红黑树的节点类 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //当前节点的父节点 TreeNode<K,V> parent; //左孩子节点 TreeNode<K,V> left; //右孩子节点 TreeNode<K,V> right; //指向前一个节点 TreeNode<K,V> prev; // needed to unlink next upon deletion //当前节点是红色或者黑色的标识 boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } } HashMap 构造函数HashMap有四个构造函数可供我们使用,一起来看下:
//默认无参构造,指定一个默认的加载因子 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } //可指定容量的有参构造,但是需要注意当前我们指定的容量并不一定就是实际的容量,下面会说 public HashMap(int initialCapacity) { //同样使用默认加载因子 this(initialCapacity, DEFAULT_LOAD_FACTOR); } //可指定容量和加载因子,但是笔者不建议自己手动指定非0.75的加载因子 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; //这里就是把我们指定的容量改为一个大于它的的最小的2次幂值,如传过来的容量是14,则返回16 //注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢? //先卖个关子,等到 resize 的时候再说 this.threshold = tableSizeFor(initialCapacity); } //可传入一个已有的map public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } //把传入的map里边的元素都加载到当前map final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); //put方法的具体实现,后边讲 putVal(hash(key), key, value, false, evict); } } } tableSizeFor()