HashMap1.7和1.8区别
- 数据结构
- 插入数据方式
- hash计算规则(扩容时数组存储位置)不一样
- 扩容与插入数据顺序
1.7
-
数组+链表的数据结构
-
插入数据时使用头插法(新来的值会取代原有的值,原有的值就顺推到链表中去)。
-
扩容时数组存储位置****hash值和需要扩容的二进制数进行&,h & (length-1)
-
插入数据前扩容
1.8
- 数组+链表+红黑树的数据结构。**当链表的长度达到8,也就是默认阈值以及table长度大于 MIN_TREEIFY_CAPACITY(节点被树化时的最小的hash表容量)时。**自动扩容把链表转成红黑树来把时间复杂度从O(n)变成O(logN)提高了效率)
- 插入数据时使用尾插法
- 低位原始位置,高位扩容前的原始位置+扩容的大小 高位:newTab[j + oldCap] = hiHead; 低位newTab[j] = loHead;
- 插入数据成功后扩容
为什么把头插法改为尾插法
**1.8中HashMap把链表转化为红黑树的阈值是8,**红黑树化为链表为6
如果选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。
源码部分就那put方法来展示,put方法比较具备代表性。
HashMap中的常量
-
DEFAULT_INITIAL_CAPACITY = 1 << 4; HashMap的默认容量 16
-
MAXIMUM_CAPACITY = 1 << 30; HashMap的最大容量 2^30
-
DEFAULT_LOAD_FACTOR = 0.75f; 负载因子。默认0.75.扩容时使用
-
TREEIFY_THRESHOLD = 8; 数组某个下标对应的链表长度大于该默认值,转化为红黑树
-
UNTREEIFY_THRESHOLD = 6; 数组某个下标对应的链表长度小于该默认值,转化为链表
-
MIN_TREEIFY_CAPACITY = 64; 节点被树化时的最小的hash表容量(当桶中node的数量大到需要变为红黑树时,若hash表的容量小于MIN_TREEIFY_CAPACITY容量时需要进行resize()扩容。操作MIN_TREEIFY_CAPACIT的值至少是TREEIFY_THRESHOLD 的4倍)
-
Entry<K,V>[] table / Node<K,V>[] table; 存储元素的数组。总是2的n次幂
-
Set<Map.Entry<K,V>> entrySet; 存储集体元素的容器
-
int size; 存储键值对的数量
-
int modCount; hashMap扩容和结构改变的次数
-
int threshold; 扩容的临界值(容量 * 负载因子)
-
float loadFactor; 负载因子
JDK7中的HashMap
说一下jdk7的版本jdk1.7.0_80。虽说都是1.7,但是代码还是不太一样。
初始化HashMap map = new HashMap<>();
实例化的时候校验了初始化容量和负载因子。并没有去初始化table数组
put方法源码
**map.put("key", "value"); **
- inflateTable(threshold); 初始化table及参数值--> 第一次put
-
roun dUpToPowerOf2 该方法为找到大于toSize的最小的2的幂数
-
threshold 初始化扩容的临界值,扩容时使用
-
table = new Entry[capacity]; 创建数组
-
putForNullKey(value);
-
存放key值为null的键值对。首先判断是否存在key。如果存在,则替换,返回替换之前的值。不存在则存储在数组下标为0的位置,hash值为0。执行addEntry方法。
-
hash = hash(key); indexFor(hash, table.length); 获取key对应的数组的索引
-
for (Entry<K,V> e = table[i]; e != null; e = e.next) 判断是否存在key值,存在则替换Value,返回替换之前的Value值
-
addEntry(int hash, K key, V value, int bucketIndex)
-
如果满足扩容条件(size >= threshold) && (null != table[bucketIndex]),则进行扩容
-
之后添加到链表中(头插法)
-
-
Entry<K,V> e = table[bucketIndex]; 保存当前节点
-
table[bucketIndex] = new Entry<>(hash, key, value, e); 新建entry,next指向当前节点
put方法总结
-
判断是否是第一次执行put方法。如果是,初始化table以及threshold(扩容的临界值)。
-
key==null,默认存储在数组索引为0的位置。判断是否存在key对应的value。存在则替换,返回替换前的value。不存在则判断是否满足扩容条件。之后使用头插法把key和value插入到链表
-
key!=null,根据key得到hash值,然后 hash & table.length,得到存放数据的数组索引i。判断是否存在key对应的value。存在则替换,返回替换前的value。不存在则判断是否满足扩容条件。之后使用头插法把key和value插入到链表、
Resize方法源码
扩容时2 * table.length
-
如果当前容量是MAXIMUM_CAPACITY,此方法不调整table的大小,但是将阈值设置为Integer.MAX_VALUE。 防止以后调用。
-
否则新建entry,容量为当前容量的两倍。
-
void transfer(Entry[] newTable, boolean rehash)
-
-
转移table数据到newTable。默认情况下不会进行rehash。重新计算entry对应的数组的索引。扩容时也是头插法
Resize方法总结
- 如果当前容量是MAXIMUM_CAPACITY,此方法不调整table的大小,但是将阈值设置为Integer.MAX_VALUE。 防止以后调用。
- 否则新建entry,容量为当前容量的两倍。
- 转移table数据到newTable。默认情况下不会进行rehash。重新计算entry对应的数组的索引
- 重新计算threshold(扩容的临界值)
JDK8
初始化HashMap
Map<String, String> map = new HashMap();
-
tableSizeFor(cap) 查找大于cap的最小的2倍幂
static final int tableSizeFor(int cap) { //18 // 容量减1 为了防止传递的cap刚好为2倍幂 int n = cap - 1; // 17 -> 00010001 n |= n >>> 1; // 00011001 n |= n >>> 2; // 00011111 n |= n >>> 4; // 00011111 n |= n >>> 8; // 00011111 n |= n >>> 16; // 00011111 // 执行以上代码是为了让cap二进制的最高位到最末位都为1 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
put方法源码
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- hashMap重写的hash函数。hashMap可以存储null,并且固定存储在数组的0下标
目前对红黑树理解不足,无法描述其执行流程。以后补充。
@6:treeifyBin方法内部会判断当前table长度是否小于 MIN_TREEIFY_CAPACITY(节点被树化时的最小的hash表容量)。小于的话就执行resize()方法。否则转为红黑树
put方法总结
-
判断是否是第一次执行put方法。如果是,扩容。
-
计算key1的hash值对应的数组下标i。
-
table[i]为null,直接新建节点指向当前table位置,然后查看是否满足扩容条件,满足就扩容,不满足返回null。
-
table[i]不为null。判断table[i]节点与传入的key是否一致。
-
一致,记录当前节点value为oldValue,然后覆盖当前节点的value值,返回oldValue
-
不一致。如果当前节点类型为TreeNode。如果有对应的key,返回节点。没有返回null。否则遍历链表,如果出现key一致,记录节点返回。没有遍历到最后,根据key,value创建node,添加到尾部。
-
对返回的节点判断。不为null,覆盖value值,返回oldValue
Resize()扩容源码
@6: 扩容后长度为原来table得到2倍。于是通过**(e.hash & oldCap == 0)**把newTab分为两部分,高位和低位。原来链表的键值对,一般放在高位,一般在低位
**e.hash & oldCap == 0 **oldCap = 16,二进制为10000,第5位为1。e.hash & oldCap 是否等于0就取决于e.hash的第5 位是0还是1,这就相当于有50%的概率放在新hash表低位,50%的概率放在新hash表高位。
Resize()方法总结
- oldTab为null时,需要初始化table,使用默认值计算table的容量和扩容的临界值,创建newTable并返回
- oldTab不为null时,判断容量是否是最大值,是就让扩容的临界值等于最大值,防止以后调用。不是就扩容为原来容量的2倍。并计算出扩容的临界值。创建newTable,遍历oldTable,转移数据并返回newTable