高手过招,招招致命
JDK1.8 中 HashMap 的底层实现,我相信大家都能说上来个 一二,底层数据结构 数组 + 链表(或红黑树) ,源码如下
/**
* 数组
*/
transient Node<K,V>[] table;
/**
* 链表结构
*/
static class Node<K,V> implements Map.Entry<K,V> {
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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
/**
* 红黑树结构
*/
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;
...
但面试往往会问的比较细,例如下面的容量问题,我们能答上来几个?
1、table 的初始化时机是什么时候,初始化的 table.length 是多少、阀值(threshold)是多少,实际能容下多少元素
2、什么时候触发扩容,扩容之后的 table.length、阀值各是多少?
3、table 的 length 为什么是 2 的 n 次幂
4、求索引的时候为什么是:h&(length-1),而不是 h&length,更不是 h%length
5、 Map map = new HashMap(1000); 当我们存入多少个元素时会触发map的扩容;Map map1 = new HashMap(10000); 我们存入第 10001个元素时会触发 map1 扩容吗
6、为什么加载因子的默认值是 0.75,并且不推荐我们修改
由于我们平时关注的少,一旦碰上这样的 连击 + 暴击,我们往往不知所措、无从应对;接下来我们看看上面的 6 个问题,是不是真的难到无法理解 ,还是我们不够细心、在自信的自我认为
斗智斗勇,见招拆招
上述的问题,我们如何去找答案 ? 方式有很多种,用的最多的,我想应该是上网查资料、看别人的博客,但我认为最有效、准确的方式是读源码
问题 1:table 的初始化
HashMap 的构造方法有如下 4 种
/**
* 构造方法 1
*
* 通过 指定的 initialCapacity 和 loadFactor 实例化一个空的 HashMap 对象
*/
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);
}
/**
* 构造方法 2
*
* 通过指定的 initialCapacity 和 默认的 loadFactor(0.75) 实例化一个空的 HashMap 对象
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 构造方法 3
*
* 通过默认的 initialCapacity 和 默认的 loadFactor(0.75) 实例化一个空的 HashMap 对象
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
*
* 构造方法 4
* 通过指定的 Map 对象实例化一个 HashMap 对象
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
构造方式 4 和 构造方式 1 实际应用的不多,构造方式 2 直接调用的 1(底层实现完全一致),构造方式 2 和 构造方式 3 比较常用,而最常用的是构造方式 3;此时我们以构造方式 3 为前提来分析,而构造方式 2 我们则在问题 5 中来分析
使用方式 1 实例化 HashMap 的时候,table 并未进行初始化,那 table 是何时进行初始化的了 ?平时我们是如何使用 HashMap 的,先实例化、然后 put、然后进行其他操作,如下
Map