前言
相信点到我这篇文章的同学们应该不是第一次看到关于HashMap的文章,网上的 HashMap 文章大多不适合自己,所以我决定自己从头开始理解源码,我会用说服自己的相对简单的逻辑去理解源码。如果有兴趣请再往下看一点吧
0.认识HashMap的类头
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap继承了AbstrasctMap以此获得了一些常用的Map Api 比如说 containsValue put set 等等
cloneable接口只是一个标识,HashMap对clone方法进行的重写,方法返回值是HashMap实例(对象)的副本。具体可以看:HashMap的clone方法
Serializable接口标识HashMap可以被序列化传输,其中有两个关于Serializable 的方法,我没有深入去看有需要的同学可以自己钻研一下
/**
* Save the state of the <tt>HashMap</tt> instance to a stream (i.e.,
* serialize it).
*
* @serialData The <i>capacity</i> of the HashMap (the length of the
* bucket array) is emitted (int), followed by the
* <i>size</i> (an int, the number of key-value
* mappings), followed by the key (Object) and value (Object)
* for each key-value mapping. The key-value mappings are
* emitted in no particular order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}
/**
* Reconstitute the {@code HashMap} instance from a stream (i.e.,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
1.HashMap主要参数解释
先记下参数的作用,之后看代码的时候就会事半功倍
/*HashMap的默认最大容量 1<<4 == 16*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/*Hash Map的最大容量 2^30 , 但是这个不是HashMap的真正的最大容量?*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/*默认加载因子,传参的时候可以自己决定加载因子大小*/
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;
/*记录HashMap中元素的个数*/
transient int size;
/*用来记录HashMap内部结构发生变化的次数*/
transient int modCount;
/*表示所能容纳的键值对的临界值 超过这个值hashmap就要扩容
计算公式 = 数组长度 * 加载因子(loadFactor)*/
int threshold;
/*加载因子 默认为0.75f*/
final float loadFactor;
//存储元素的数组,存的是链表,这个数组的长度必须是二的次方数
transient Node<K,V>[] table;
综上: 用一张图来阐述上面几个参数的关系
2.Node类的认识
//我认为的关键部分就这么多
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;}
}
//hash算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
传入node 的传入的值是通过Node 的 key值经过一次扰动 得到,但是hash化之后一定会出现hash碰撞(hash碰撞比较复杂会单独写一个解说) hashmap就会采用数组加链表的形式来存储冲突数据就像下图(来源 B站暴躁的小刘老师)
链表长度大于8之后且节点个数大于64就换会转成红黑树 ,当节点个数小于6时又会退化成链表,原因就是链表的搜索时间复杂度时O(n) 红黑树是 O(nlogn) 树的搜索比链表快一些
那就有了一个问题,为什么树搜索这么快不直接用树呢?
我觉得是因为红黑树的插入成本删除成本的时间代价比链表大的多,所以才会选用链表加红黑树.说到底一切都是为了提高效率
阶段性小结
看源码真的很难很痛苦,但是在难的代码也是人写的,他们能写,我们就也能写.不过目前还是多看大佬们写的代码多多学习多模仿,争取早日能企及到大佬的高度
接下来的目标
接下来几篇应该会对HashMap的主要几个函数源码进行解析
等我理清之后就会在这里补充链接
从零开始的 HashMap 源码阅读(二) 构造函数分析
也希望看到篇文章的人能对这篇文章提出意见无论是那里写的不清楚那里需要扩充都可以在评论区告诉我,相互帮助共同成长
收藏加关注,下次再来不迷路.我会持续更新