字节面试官问我,Java 的 HashMap 的源码看过吗?我???我花了十分钟给他解释的清清楚楚的。
先看再点赞,给自己一点思考的时间,微信搜索【沉默王二】关注这个有颜值却假装靠才华苟且的程序员。
本文 GitHub github.com/itwanger 已收录,里面还有一线大厂整理的面试题,以及我的系列文章。
List 系列差不多写完了, 单线程环境下最重要的就是 ArrayList 和 LinkedList,多线程环境下最重要的就是 CopyOnWriteArrayList,新来的同学可以点击链接回顾一下 List 的知识点。接下来,我要带着 HashMap 去爬山了,注意不是六峰山,纯粹就是为了锻炼了一下身体,不不不,纯粹是为了和 HashMap 拉近关系,同学们注意不要掉队。
说一句很废的话,HashMap 是一个 Map,用来存储 key-value 的键值对,每个键都可以精确地映射到一个值,然后我们可以通过这个键快速地找到对应的值。
对于一个 List 来说,如果要找到一个值,时间复杂度为 O ( n ) O(n) O(n),如果 List 排序过的话,时间复杂度可以降低到 O ( l o g n ) O(log n) O(logn)(二分查找法),但如果是 Map 的话,大多数情况下,时间复杂度能够降低到 O ( 1 ) O(1) O(1)。
来看一下 HashMap 的特点:
HashMap 的键必须是唯一的,不能重复。
HashMap 的键允许为 null,但只能有一个这样的键;值可以有多个 null。
HashMap 是无序的,它不保证元素的任何特定顺序。
HashMap 不是线程安全的;多线程环境下,建议使用 ConcurrentHashMap,或者使用 Collections.synchronizedMap(hashMap) 将 HashMap 转成线程同步的。
只能使用关联的键来获取值。
HashMap 只能存储对象,所以基本数据类型应该使用其包装器类型,比如说 int 应该为 Integer。
HashMap 实现了 Cloneable 和 Serializable 接口,因此可以拷贝和序列化。
01、HashMap 的重要字段
HashMap 有 5 个非常重要的字段,我们来了解一下。(JDK 版本为 14)
transient Node<K,V>[] table;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
1)table 是一个 Node 类型的数组,默认长度为 16,在第一次执行 resize() 方法的时候初始化。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
final HashMap.Node<K,V>[] resize() {
newCap = DEFAULT_INITIAL_CAPACITY;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
}
Node 是 HashMap 的一个内部类,实现了 Map.Entry 接口,本质上是一个键值对。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; HashMap.Node<K,V> next; Node(int hash, K key, V value, HashMap.Node<K,V> next) { ... } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { ... } public final V setValue(V newValue) { ... } public final boolean equals(Object o) { ... } }
2)size 就是 HashMap 中实际存储的键值对数量,它和 table 的 length 是有区别的。
为了说明这一点,我们来看下面这段代码:
HashMap<String,Integer> map = new HashMap<>();
map.put("1", 1);
1
2
声明一个 HashMap,然后 put 一个键值对。在 put() 方法处打一个断点后进入,等到该方法临近结束的时候加一个 watch(table.length),然后就可以观察到如下结果。
也就是说,数组的大小为 16,但 HashMap 的大小为 1。
3)modCount 主要用来记录 HashMap 实际操作的次数,以便迭代器在执行 remove() 等操作的时候快速抛出 ConcurrentModificationException,因为 HashMap 和 ArrayList 一样,也是 fail-fast 的。
关于 ConcurrentModificationException 的更多信息,请点击下面的链接查看 03 小节的内容。
4)threshold 用来判断 HashMap 所能容纳的最大键值对数量,它的值等于数组大小 * 负载因子。默认情况下为 12(16 * 0.75),也就是第一次执行 resize() 方法的时候。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
final HashMap.Node<K,V>[] resize() {
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
5)loadFactor 为负载因子,默认的 0.75 是对空间和时间效率上的一个平衡选择,一般不建议修改,像我这种工作了十多年的老菜鸟,就从来没有修改过这个值。