HashMap的存储与实现
我们如果要保存一组对象,用我们之前学过的知识,会使用对象数组,但鉴于数组的局限性,数组长度一经定义就不能改变,所以我们使用链表、队列等数据结构操作,但是很麻烦。类集框架就是一个动态的数组,但不受数组长度的限制。
HashMap允许key值为空,(在方法containsValue(Object value):如果指定值key==null,并且在键值对中有value为null时,也返回true)但是Hashtable不允许,否则会报“NullPointer Expection”异常。
一、HashMap键值对的实现
HashMap是Map接口的实现子类,用于存放一对值,即类中的每一个元素都是以Key---->Value的形式存储。我们知道,在Java集合框架中,无论是将一个对象存放在数组中,还是队列中,其实并不是把这个对象存入其中,而是将对象的引用存入数组或者队列中。在HashMap中,数据的存储同样如此,我们通过调用put(K key,V value)方法存储键值对,方法如下:
Java代码
- /**
- * 存储关联的键值对
- * @param key:键
- * @param value:值
- * @return
- */
- public V put(K key, V value) {
- //当键值为null时,调用putForNullKey(value)的方法存储,
- //在该方法中调用recordAccess(HashMap<K,V> m)的方法处理
- if (key == null)
- return putForNullKey(value);
- //根据key的KeyCode,计算hashCode
- int hash = hash(key.hashCode());
- //调用indexFor方法,返回hash在对应table中的索引(Entry[] table)
- int i = indexFor(hash, table.length);
- //当i索引处的Entry不为null时,遍历下一个元素
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- Object k;
- //如果遍历到的hash值等于根据Key值计算出的hash值并且
- //key值与需要放入的key值相等时,存放与key对应的value值
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- //覆盖oldValue的值
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- //当i索引处的Entry为null时,将指定的key、value、hash条目放入到指定的桶i中
- //如果现有HashMap的大小大于容量*负载因子时,resize(2 * table.length);
- addEntry(hash, key, value, i);
- return null;
- }
在上面的put(K key,V value)方法中可知,当要存储Key---->Value对时,实际上是存储在一个Entry的对象e中,程序通过key计算出Entry对象的存储位置。换句话说,Key---->Value的对应关系是通过key----Entry----value这个过程实现的,所以就有我们表面上知道的key存在哪里,value就存在哪里。在Map接口中,有一个Entry接口,该接口用于处理key和value的set()和get()方法,所以在Map中存储数据,实际上是将Key---->value的数据存储在Map.Entry接口的实例中,再在Map集合中插入Map.Entry的实例化对象,如图示:
二、HashMap的存储机制
HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。
HashMap有四种方法:
HashMap():初始容量16,默认加载因子0.75
HashMap(int initialCapacity):自定义初始容量
HashMap(int initialCapacity,float loadFactor):自定义初始容量和加载因子
HashMap(Map<? extends K,? extends V> m)
这四个构造方法其实都受两个参数的影响:容量和加载因子。容量是哈希表中桶的数量,初始容量为16。加载因子是对哈希表的容量在自动增加resize()之前所达到尺度的描述。当哈希表中的条目数超过threshold(=Capacity*loadFactor) 的值时,要对哈希表进行rehash操作。
默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
三、HashMap的冲突处理问题
由于哈希函数是一个压缩映象,因此在一班情况下,很容易产生“冲突”现象,即key1 ≠ key2,而f(key1)=f(key2)。而且,由于关键字的集合比较大,这种冲突是不可避免的,所以必须采取合理的解决方案,找出尽量少产生冲突的哈希函数和处理冲突的方法。对于哈希函数的构造,通常有直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法等。而这里重点讲述处理冲突的两种方法。
1、 开放地址法
开放地址法是对那些发生冲突的记录,用hi=(h(key)+di)mod n方法再次确定Hash地址。
n:为哈希表长;
di:为增量序列,其取法有以下三种:
1)线性探测再散列 di= c * i
2)二次探测再散列 di = 12, -12, 22, -22, …,
3) 随机探测再散列 di是一组伪随机数列 或者 di=i×H2(key) (又称双散列函数探测)
例如表长为11的哈希表中已填有关键字为17,60,29的记录,H(key)=key MOD 11,现有第4个记录,其关键字为38
H(38)=38 MOD 11=5 冲突
H1=(5+1) MOD 11=6 冲突
H2=(5+2) MOD 11=7 冲突
H3=(5+3) MOD 11=8 不冲突
对于其他增量序列的方法也是如此计算。
2、链地址法
将所有哈希地址相同的记录都链接在同一链表中图形类似于图2。也就是说,当HashMap中的每一个bucket里只有一个Entry,不发生冲突时,Hashmap是一个数组,根据索引可以迅速找到Entry,但是,当发生冲突时,单个的bucket里存储的是一个Entry链,系统必须按顺序遍历每个Entry,直到找到为止。为了减少数据的遍历,冲突的元素都是直接插入到第一个Entry后面的,所以,最早放入bucket中的Entry,位于Entry链中的最末端。这从put(K key,V value)中也可以看出,在同一个bucket存储Entry链的情况下,新放入的Entry总是位于bucket中。
四、HashMap元素的输出
对于Map接口来说,其本身是不能直接使用迭代(Iteraor)进行输出的,因为Map接口的中的每个位置存放的是一对值(key---->value),而Iterator中每次只能找到一个值,如果要通过迭代的方法进行输出,主要分为以下几步:
1、将Map接口的实例通过Set<Entry<K,V>> entrySet();方法变为Set接口对象;
2、通过Set接口实例为Iterator实例化
3、通过Iterator迭代输出,输出的每个内容都是Map.Entry的对象
4、通过Map.Entry进行key---value的分离
具体代码实现如下:
Java代码
- /实例化HashMap对象
- HashMap<String,String> hashMap=new HashMap<String,String>();
- //1、将Map接口变为Set接口
- Set<Map.Entry<String,String>> set=hashMap.entrySet();
- //2、实例化Iterator接口
- Iterator it=set.iterator();
- while(it.hasNext()){
- //3、得到存储在HashMap中的Entry对象
- Map.Entry<String,String> me=(Entry<String, String>) it.next();
- //4、通过Entry得到key和value
- System.out.println("Key="+me.getKey()+"Value="+me.getValue());
- }
上面的Map的输出过程,entrySet()主要是返回此映射所包含的映射关系的 Set 视图,在HashMap中,还有一个keySet()方法用于返回此映射中所包含的键的 Set 视图,步骤都是一样的。根据key可以通过get(key)方法找到对应的value。如果存储的key值不是系统类,而是自定义的类,则需要注意以下两点:
1)必须存储自定义类的实例化对象,如果使用匿名对象,就找不到对应值。
例如,key值是一个Student的类型
Java代码
- HashMap<Student,String> map=new HashMap<Student,String>();
- map.put(new Student("1608100201","Jony"), "CSU");
- System.out.println(map.get(stu));
这段代码是无法找到对应的value值的,会输出null;正确的代码应该是下面的写法,才能找到value值,因为在设置和取得的过程中,都使用的是Student的实例化对象,地址没有变化。
Java代码
- //实例化一个学生对象
- Student stu=new Student("1608100201","Jony");
- HashMap<Student,String> map=new HashMap<Student,String>();
- map.put(stu, "CSU");
- System.out.println(map.get(stu));
2)覆写equals()和hashCode()方法。我们在使用时,要想明确的知道其中一个key的引用地址,就得依靠这两个方法。
Java代码
- public class Student {
- //学生的学好属性
- public static String ID;
- //学生的姓名属性
- private String name;
- /*
- * 重载构造方法
- */
- public Student(String ID,String name){
- this.ID=ID;
- this.name=name;
- }
- /**
- * 覆写equals()方法
- */
- public boolean equals(Object obj) {
- //判断地址是否相等
- if(this==obj){
- return true;
- }
- //传递进来用于比较的对象不是本类的对象
- if (!(obj instanceof Student))
- return false;
- //向下转型
- Student stu = (Student)obj;
- //比较属性内容是否相等
- if (this.ID.equals(stu.ID)&&this.name.equals(stu.name)) {
- return true;
- }
- return false;
- }
- /**
- * 覆写hashCode()方法
- */
- public int hashCode() {
- return this.ID.hashCode();
- }
- }
五、HashMap源码分析及1.6,1.7,1.8之间的区别(重点)
hashMap源码特别是1.8加入了红黑树,导致源码的难易程度大大升级
1.简介
HashMap最早出现在JDK1.2中,底层基于散列算法实现。HashMap 允许 null 键和 null 值,是非线程安全类,在多线程环境下可能会存在问题。
2.HashMap各个版本之间的变化
jdk1.8版本的HashMap相比于1.7的变化:
- Entry结构变成了Node结构, hash变量加上了final声明 ,即不可以进行rehash了
- 插入节点的方式从 头插法 变成了 尾插法
- 引入了 红黑树
- tableSizeFor方法、hash算法等等
jdk1.7版本的HashMap相比于1.6的变化:
- 加入了jdk.map.althashing.threshold这个jdk的参数用来控制是否在扩容时使用String类型的新hash算法。
- 把1.6的构造方法中对表的初始化挪到了put方法中。
- 1.6中的tranfer方法对旧表的节点进行置null操作(存在多线程问题),1.7中去掉了。
2.HashMa1.8源码重点梳理
HashMap作为最常使用的集合之一;JDK1.7之前,有很大的争议,一方面是数据量变大之后的查询效率问题,还有就是线程安全问题。
HashMap1.8版本的源码分析:
jdk1.8版本的HashMap相比于1.7的变化:
- Entry结构变成了Node结构, hash变量加上了final声明 ,即不可以进行rehash了
- 插入节点的方式从 头插法 变成了 尾插法
- 引入了 红黑树
- tableSizeFor方法、hash算法等等
存储的基本结构为数组+链表,jdk1.8中加入了红黑树,具体转换情况下面介绍:
1)jdk1.8版本HashMap成员属性如下:
1 /**
2 * 默认初始容量16(必须是2的幂次方)--1.8把数组的初始化(空构造函数)放在了put中
3 */
4 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
5
6 /**
7 * 最大容量,2的30次方
8 */
9 static final int MAXIMUM_CAPACITY = 1 << 30;
10
11 /**
12 * 默认加载因子,用来计算threshold(threshold--扩容的阈值大小)
13 */
14 static final float DEFAULT_LOAD_FACTOR = 0.75f;
15
16 /**
17 * 链表转成树的阈值,
当桶中链表长度大于8并且数组长度小于MIN_TREEIFY_CAPACITY时转成树,
(将所有的节点转换成树形节点,并且构造成双链表 为treeify 转换成红黑树准备。)
否则进行扩容
18 threshold = capacity * loadFactor
19 */
20 static final int TREEIFY_THRESHOLD = 8;
21
22 /**
23 * 进行resize操作时,若桶中数量少于6则从树转成链表
24 */
25 static final int UNTREEIFY_THRESHOLD = 6;
26
27 /**
28 * 桶中结构转化为红黑树对应的table的最小值
29
30 当需要将解决 hash 冲突的链表转变为红黑树时,
31 需要判断下此时数组容量,
32 若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )
33 导致的 hash 冲突太多,则不进行链表转变为红黑树操作,
34 转为利用 resize() 函数对 hashMap 扩容
35 */
36 static final int MIN_TREEIFY_CAPACITY = 64;
37 /**
38 保存Node<K,V>节点的数组
39 该表在首次使用时初始化,并根据需要调整大小。 分配时,
40 长度始终是2的幂。
41 */
42 transient Node<K,V>[] table;
43
44 /**
45 * 存放具体元素的集
46 */
47 transient Set<Map.Entry<K,V>> entrySet;
48
49 /**
50 * 记录 hashMap 当前存储的元素的数量
51 */
52 transient int size;
53
54 /**
55 * 每次更改map结构的计数器
56 */
57 transient int modCount;
58
59 /**
60 * 临界值(阈值) 当实际大小(容量*填充因子)超过临界值时,会进行扩容
61 */
62 int threshold;
63
64 /**
65 * 负载因子(哈希表的加载因子,一般情况下会设置为DEFAULT_LOAD_FACTOR)
66 */
67 final float loadFactor;
LoadFactor负载因子解释:
HashMap中负载因子是个很重要的参数,反应了 HashMap 桶数组的使用情况。通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。
详解:
当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。
相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。至于负载因子怎么调节,这个看使用场景了。
一般情况下,我们用默认值就可以了。大多数情况下0.75在时间跟空间代价上达到了平衡所以不建议修改
2)jdk1.8版本的数据存储结构
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; }
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;
}
}
TreeNode 红黑树数据结构:
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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
...
3)1.8版本的HashMap构造函数
4种(重要点:初始化用户自定义的加载银子loadFactor)
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);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//参数为Map的构造方法,先计算需要的容量大小,然后调用putVal方法插入节点
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
4)jdk1.8HashMap的几个重要方法详解:
重点:存放put(),取值get(),移除remove(),扩容resize(),以及两个内部方法(tableSizeFor()与简化的
hash算法)
①put(K key,V value)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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为空,调用扩容方法
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(n - 1) & hash确定元素存放在桶中的下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//比较桶中第一个元素(数组结点)的hash值与key值
//若相等,则将e指向该键值对,否则需要判断桶中是链表还是红黑树
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 {
//若为聊表,则需要遍历链表,并统计链表结构,判断节点数量是否达到转红黑树阈值
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;
}
//若未到达链表尾部,需判断此结点key值与待插入元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//此时e的位置为要插入元素的位置,判断要插入的键值对是否存在于HashMap中
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//LinkedHashMap重写使用,会根据accessOrder(若为false则按照插入顺序(默认),否则按照访问顺序true--最少访问的靠前,最新访问的靠后)该方法一般在key值相同更新结点时调用
return oldValue;
}
}
++modCount;
//若当前HashMap容量超过阈值时,需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);//LinkedHashMap重写使用
return null;
}
②get(Object key)方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode方法很简单,(n - 1) & hash计算key值对应的table下标,找到链表,先判断头节点,然
后循环查找,如果头节点是树节点,调用树节点的getTreeNode方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
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;
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;
}
③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) { //旧表已经初始化
if (oldCap >= MAXIMUM_CAPACITY) { //1.达到上限,不再扩容
threshold = Integer.MAX_VALUE;
return oldTab;
}//2.若旧表容量>=16,并且*2还小于上限,扩容2倍(新表与新阈值)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}//旧表未初始化且旧阈值>0,则新表容量为阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults //旧表未初始化且旧阈值=0 此时都为默认值(16,0.75)
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"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//若旧表中存在数据,则涉及到表中值的迁移
/**
* 如果旧表里有值,需要把旧表里的值重新计算放到新表里
* hash & (oldCap*2-1)计算新表中的位置,只可能得到两种结果(把新表分成两个小表)
* hash & (oldCap-1)代表放在前面的表里hash & (oldCap-1) + oldCap 放在后面的表里
* hash & oldCap == 0 就是第一种结果, !=0 就是第二种结果
*/
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)//若数组中结点处只有一个元素,则直接赋值
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;
}
④remove(Object key) 方法
@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
⑤tableSizeFor(int cap)方法
该方法被调用三次
该方法目的:用位运算 找到大于或等于输入参数的最近的2的整数次幂的数。比如10,则返回16。
具体详解参看:https://www.cnblogs.com/loading4/p/6239441.html
⑥1.8版本的hash算法
1.8版本hash算法进行了简化,直接把高16位移下来进行或运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.7版本hash算法:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
1.6版本的hash算法:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
HashMap疑问和进阶:
1.Hash()作用:
它的目的是让“1”变的均匀一点,散列的本意就是要尽量均匀分
当计算出来的hash函数h和hashMap的length做了&运算后,会得到[0,length-1]其中的一个值,而散列的均匀也会使这个值分布的均匀,从而达到HashMap高效的一点(当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,简单而效率高)
hash对一个对象的hashCode进行重新计算,而IndexFor生成这个对象的index。
2.hash值重新计算目的:
防止质量低下的hashCode()函数实现。在hashMap数组长度中长度是初始长度的2倍。通过右移造成地位的数据尽量的不同。
3.jdk1.8的HashMap为什么使用红黑树,而不用平衡二叉树?
插入效率比平衡二叉树高,查询效率比普通二叉树高。所以选择性能相对折中的红黑树。
4.HashMap为什么不直接使用对象的原始hash值呢?
通过移位和异或运算,可以让 hash 变得更复杂,进而影响 hash 的分布性。
5.既然红黑树那么好,为什么不直接使用红黑树,而是当链表数量超过8个时才转?
因为红黑树需要进行左旋,右旋操作, 而单链表不需要。
以下都是单链表与红黑树结构对比。
如果元素小于8个,查询成本高,新增成本低。如果元素大于8个,查询成本低,新增成本高。
至于为什么选数字8,是大佬折中衡量的结果-.-,就像loadFactor默认值0.75一样。
关于HashMap源码各个版本的对比参考下文:
https://blog.csdn.net/boom_man/article/details/78286610
https://www.codercto.com/a/12460.html