面试题1:谈谈HashMap的基本实现?
HashMap的基本数据结构为自定义的Node内部静态类组成的数组,该静态类存有四个变量,即hash值,key-value对和向后的节点,jdk1.7及以前为单向链表数组,jdk1.8及以后为单向链表数组和红黑树的混合数组,当hash冲突小于8时为单向链表,大于等于8时会转化成黑红树,默认初始最大容量为0,装载因子为0.75f,第一次扩容的默认最大容量为16,以后每次扩容容量翻倍。
面试题2:为什么HashMap是否线程安全,为什么?
HashMap线程不安全主要有两种情况,第一种情况:多个线程同时put同一个Hash冲突时,可能会出现后面的线程覆盖前面的线程的值。第二种情况:一个线程扩容,其他线程get同一个Hash冲突时,有可能在get线程种形成闭环,就会进入死循环。
面试提3:什么是红黑树,有哪些基本特征?
红黑树是一种自平衡的二叉查找树,主要有以下特征
1.节点是红色或黑色。
2.根节点是黑色。
3.每个叶子节点都是黑色的空节点(NIL节点)。
4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
红黑树能保证从根节点到叶子节点的最长路径不会超过最短路径的2倍,写操作时,红黑树通过变色和自旋调整保证红黑树的基本特性。
源码解析:此源码基于jdk1.8,与jdk1.7有较大的区别,有兴趣的读者可以自己去看基于jdk1.7及以前的源码。
1.HashMap
(1)类定义
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap实现了Cloneable(复制), Serializable(序列化)接口
其主要继承和实现关系如下:
HashMap-->AbstractMap-->Map
HashMap-->Map
前面那条线是对Map的一些优化
后面那条线是Map的基本线
(2)主要变量
transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; transient int size; 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; }
HashMap的主要存储结构是自定义的内部静态类的键值对数组。
(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() { this.loadFactor = DEFAULT_LOAD_FACTOR; } static final int MAXIMUM_CAPACITY = 1 << 30; static final float DEFAULT_LOAD_FACTOR = 0.75f;
由上面的构造方法可知,未设置初始值时,初始容量threshold会默认为0,并设置初始加载因子loadFactor=0.75,该加载因子和初始容量均可设置,loadFactor必须是float类型,且最大容量为1 << 30。
(4)主要方法
Hash算法
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
HashMap的hash算法为自身hashcode与自身hashcode右移16位做异或运算,如果没有重写hashcode或者是null对象,hash算法的结果是0。
Put操作
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
put方法比较长,由于篇幅有限,只做原理分析
1.执行hash算法,取到key的hash值。
2.检测table是否位空,如果为空会执行resize方法,设置基本的初始值,所以,如果new HashMap不带参时,会让第一次put变复杂。
3.找到table中对应hash值的节点,其算法是hash值与table长度-1做位与运算,如果没有,会新建一个node节点,并设置该节点的key、valu、hash值为put的key、value和key对应的hash值,next节点为null。
4.如果存在且节点的hash值为新put的hash值且key与该node的key相等,或者equals为true,则记录该节点,value在后面统一设置。
5.如果存在且属于树状节点,直接按树的规则遍历,如果节点存在则记录该节点,value在后面统一设置,不存在则新建节点,并设置该节点的key、valu、hash值为put的key、value和key对应的hash值,记录值为null。
6.如果存在且不属于树状节点,且小于8个,则按链表方式遍历直到尾部,节点存在则记录该节点,value在后面统一设置,不存在则新建节点,并设置该节点的key、valu、hash值为put的key、value和key对应的hash值,记录值为null。
7.如果存在且不属于树状节点,且大于等于8个,则按把链表转换成树,直接按树的规则遍历,如果节点存在则记录该节点,value在后面统一设置,不存在则新建节点并设置该节点的key、valu、hash值为put的key、value和key对应的hash值,记录值为null。
8.记录节点存在,替换value值并返回被替换的值。
9.如果size超过threshold * loadFactor,则执行resize方法扩容,返回null。
扩容
final Node<K,V>[] resize() {}
resize方法比较长,由于篇幅有限,只做原理分析
1.设置扩容后的值:如果是第一次扩容且未设置容量大小,设置最大容量为默认最大容量(16),如果并非第一次扩容,则容量为table.length右移一位,容量threshold都为最大容量(默认16)* 装载因子(默认0.75),