- HashMap的初始值
- 思考:为什么负载因子是0.75
- HashMap的存储结构
- 时间复杂度效率对比
- new 一个HashMap的一些方法
- put方法
- 思考:为什么链表长度达到8之后转变成红黑树结构?
- resize方法
- get方法
- getNode方法
HashMap的初始值
//初始化容量(必须是二的n次幂)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//集合的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子,默认0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表的值超过8,链表会转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
//当链表的值小于6会从红黑树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
/*
* 哈希表中的容量超过这个值时,才将链表转换成红黑树。否则桶内元素太多,直接扩容,而不是树形化。
* 为了避免扩容、转换红黑树之间的冲突,该值不能小于4 * TREEIFY_THRESHOLD
*/
static final int MIN_TREEIFY_CAPACITY = 64;//初始化table的大小,必须是2的n次幂
transient Node<K,V>[] table;
//存放缓存大小
transient Set<Map.Entry<K,V>> entrySet;
//HashMap的存储大小
transient int size;
//用来记录HashMap的修改次数
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
//(负载因子*容量):扩容数组的时候用来计算新容器的大小计算方式
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
//哈希表的加载因子
final float loadFactor;
//创建一个指定大小的HashMap,负载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
//创建一个空的HashMap 默认大小16,负载因子0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Returns a power of two size for the given target capacity.
*
* 返回 cap 初始值的2的幂
* 作用:将传入的容量大小转化为:>传入容量大小的最小的2的幂
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
思考:为什么负载因子是0.75
负载因子的作用是和扩容机制相关联的,HashMap初始值数组长度为16,当数组长度达到 16*0.75=12的时候就会对数组进行扩容。这个12就是通过负载因子计算出来的阈值。
那么为什么默认是0.75呢?为什么不是1,不是0.5。HashMap1.8的结构是数组+链表+红黑树组成的。在HashMap 发生哈希碰撞之后,会在同一个节点中挂一个链表添加相同的哈希值,当链表长度达到8的时候会转变成红黑树,时间复杂度从链表的O(n)变为O(logn)。如果负载因子是1,那么也就是当数组长度达到16的时候才能进行扩容,这就意味着大量的哈希冲突会出现,在遍历红黑树获取值的时候时间会更久,相当于提升了空间利用率,牺牲了时间。
相对的,如果负载因子默认是 0.5 ,数组长度达到一半就会扩容,那么空间利用率就会降低。
如果你想问为什么是0.75最合适而不是0.8呢?(大佬说是0.75就是0.75嘛,你有什么好杠的~)
HashMap的存储结构

HashCode方法发生哈希碰撞的情况下,在相同元素创建一个链表,把所有相同的元素存放在链表中

可以看出T1的哈希和T2相同,但是元素不同,所以现在会形成一个链来存储。
但是如果链表过长,HashMap会把这个链表转换成红黑树
时间复杂度效率对比
时间复杂度的优劣对比常见的数量级大小:越小表示算法的执行时间频度越短,则越优;
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n)源码的负载因子上也做了相应的解释,但是用翻译软件翻译出来的话总是有点鸡同鸭讲。大概意思就是一句话,0.75是在时间和空间上权衡之后得出的结果。
/* <p>As a general rule, the default load factor (.75) offers a good
* tradeoff between time and space costs. Higher values decrease the
* space overhead but increase the lookup cost (reflected in most of
* the operations of the <tt>HashMap</tt> class, including
* <tt>get</tt> and <tt>put</tt>). The expected number of entries in
* the map and its load factor should be taken into account when
* setting its initial capacity, so as to minimize the number of
* rehash operations. If the initial capacity is greater than the
* maximum number of entries divided by the load factor, no rehash
* operations will ever occur.*/new 一个HashMap的一些方法
//创建一个空的HashMap,指定初始化大小和负载因子
public HashMap(int initialCapacity, float loadFactor) {
//判断初始化大小是否小于0,小于0抛出IllegalArgumentException异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始容量大于集合的最大容量,将初始容量改成最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//当负载因子小于0或者是一个非数值,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//创建一个指定大小的HashMap,负载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//创建一个空的HashMap 默认大小16,负载因子0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
//设置默认负载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
put方法
- 1.8的put方法和1.7比较做了比较大的改动过,由1.7的 数组+链表 变成1.8的 数组+链表+红黑树。Node类变成了Entry类
- 使用resize() 进行扩容时,数据存储的位置会进行重新计算,原有的红黑树长度 < 6 时,会转变成链表
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
/*
* 判断table是否等于空或者等于0 是:使用resize()进行初始化
* */
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/*
* 对hash值进行定位,如果tab[i]等于null ,为null创建一个新的节点,这里是会造成线程安全问题的。
* */
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e;
K k;
//这里进入tab[i]不为空标识这个位置已经存在值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将现有的值直接替换
e = p;
//判断节点是否是红黑树
else if (p instanceof TreeNode)
//红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
/**
* 不是红黑树,那就是链表,遍历到节点最后插入,也就是尾插法
* 这里和 JDK1.7 不同,1.7使用的是头插法,永远添加到数组的头部位置,原来位置上的数向后移动
*/
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判断链表的长度是否 >= 默认阈值,是的话转换成红黑树结构
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//发现key已经存在,用新 Value 覆盖旧的
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//替换旧值的方法
afterNodeAccess(e);
return oldValue;
}
}
//记录修改次数
++modCount;
//sizi是当前集合大小,判断当前集合大小是否超过阈值,超过则调用resize()
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}思考:为什么链表长度达到8之后转变成红黑树结构?
个人认为还是和时间、空间成本有关。红黑树插入、查询的时间复杂度为O(logn),链表为插入的时间复杂度为O(1),查询O(n),根据 O(1) < O(logn) < O(n) 查询数量多时,链表 > 红黑树,为了避免结构频繁的转换,默认负载因子0.75这个值,计算出来的单个Hash槽元素个数达到8的概率极低,所以将7作为分水岭。
源码中的这一部分为
//binCount >= 8-1
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;resize方法
1.8的hashMap在数组大小大于阈值,就进行resize() 进行扩容,扩容为原来的两倍
final Node<K,V>[] resize() {
//得到当前数组
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//获取原表的大小和最大值1<<30 进行比较,大于最大值,则将原表大小改成最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//没有达到最大值,进行两倍扩容
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//初始化集合默认大小16
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"})
//新建hash数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//新hash 赋值旧 hash
table = newTab;
//开始扩容
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { //如果旧的hash数组在j结点处不为空,复制给e
oldTab[j] = null; //设置为null 方便GC回收
if (e.next == null)//如果e后面没有结点
newTab[e.hash & (newCap - 1)] = e;//获得新的存储位置
else if (e instanceof TreeNode) //如果是红黑树类型,添加到红黑树中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}get方法
get方法调用的是getNode()
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}getNode方法
final Node<K,V> getNode(int hash, Object key) {
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n;
K k;
//判断table长度大于0,根据hash查找tab中的元素不为null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//查找Node中的下一个元素
if ((e = first.next) != null) {
//如果是红黑树结构
if (first instanceof TreeNode)
//在红黑树中查找该元素
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//在链表中查找该元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}