java面试中,HashMap集合的面试基本是都会问到的。
哈希表(hash table)
也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表。
Map实现类
Java为数据结构中的映射定义了一个接口Map,主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap。
针对这4个实现类做个简单的说明:
(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的键值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,通常使用ConcurrentHashMap代替。
(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的(方法上使用悲观锁synchronized),任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会抛出ClassCastException类型的异常。
存储结构:jdk1.7是数组+链表,jdk1.8是数组+链表+红黑树。

hashmap底层结构
链表主要为了解决哈希冲突(也叫哈希碰撞)而存在的。主要是因为使用hashmap存储数据的时候,两个不同的元素(键值),通过哈希函数得出的实际存储地址相同。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap就是采用了链地址法。
JDK 1.8 对 HashMap 进行了比较大的优化,底层实现由之前的 “数组+链表” 改为 “数组+链表+红黑树”,当链表节点较少时仍然是以链表存在,当链表节点较多时(等于8,将会调用内部的treeifyBin方法)转为红黑树。
JDK 1.8数组使用了Node,它实现了Map.Entry接口,本质是就是一个映射(键值对)。
初始化几个重要的参数:
int threshold; // 所能容纳的key-value对极限
final float loadFactor; // 负载因子
int modCount; //修改的次数,用于fail-fast容错
int size; //数组大小
Node[] table的初始化数组大小(length )默认值是16,负载因子(loadFactor)默认值是0.75,threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * loadFactor。得出负载因子越大,所能容纳的键值对个数就越多。
put描述过程(查看源码更清晰)
1、判断数组是否为空,为空进行初始化,不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index。
2、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中。存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false),如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
3、如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8,大于的话链表转换为红黑树;
4、插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍
主要代码实现:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// table表该索引位置不为空,则进行查找
Node<K,V> e; K k;
// 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
for (int binCount = 0; ; ++binCount) {
// 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
// 减一是因为循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 将p指向下一个节点
}
}
// 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于LinkedHashMap
return oldValue;
}
}
++modCount;
// 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 用于LinkedHashMap
return null;
}
resize扩容机制
两个重要属性:
Capacity:HashMap当前长度。
LoadFactor:负载因子,默认值0.75f。
主要两个步骤:
1、创建一个新的Entry空数组,长度是原数组的2倍。
2、遍历原Entry数组,把所有的Entry重新Hash到新数组。
为啥要重新Hash!!!卧槽这个问题!有点知识盲区呀!有没有很懵逼!直接复制过去不香么
这里我们回想下计算index的公式:index = HashCode(Key) & (Length - 1),原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了。
代码全部贴出来着实有点恶心,就分几个步骤说了:
一、扩容:如果超过了数组的最大容量,那么就直接将阈值设置为整数最大值,然后如果没有超过,那就扩容为原来的2倍。
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
二、设置阈值:如果阈值(oldThr)已经初始化过了,新阈值(newCap)直接使用旧的阈值。如果没有初始化,那就初始化一个新的数组容量和新的阈值。最后为当前的容量阈值赋值。
//第二部分:设置阈值
else if (oldThr > 0)
newCap = oldThr;
else { // 没有初始化阈值那就初始化一个默认的容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//为当前的容量阈值赋值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
三、复制数组到新数组(这个代码就不贴了,大家有兴趣自己去研究下)