基于hashing原理,通过put(key,value)和get(key)方法存储和获取对象
当我们存储对象的时候,把键值对传给put(key,value)方法时,它调用键对象key的hashCode()方法来计算hashCode,然后找到bucket位置,来存储值对象value。
当获取对象时,也是先计算key的hashCode,找到数组中对应位置的bucket位置,然后通过key的equals()方法找到正确的键值对key-value,然后返回值对象value。
HashMap里有hash碰撞(冲突),HashMap使用链表来解决碰撞问题,当发生了碰撞,对象将会存储在链表的下一个节点中。
HashMap的每个链表中存储的是key-value对象,当两个不同的键对象key的hashCode相同时,那就发生了碰撞(冲突),那它会存储在同一个bucket的位置,bucket的位置存不下这两个点,那么就通过链表来存储。(在jdk8里,链表长度大于8的话就会变成红黑树)
取数据可通过键对象key的equals()方法用来找到正确的键值对key-value。
HashMap底层数据结构JDK8以前的HashMap的底层数据结构是: 数组+链表,它之所以有相当快的查询速度,主要是因为它是通过Key计算hashCode来决定一维数组中存储的位置,而增删速度靠的是链表保证。
简单来说,HashMap由数组+链表组成的,Entry数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
JDK8中HashMap的底层数据结构:用**数组+链表+红黑树的结构来优化,链表长度大于8同时满足HashMap中元素个数大于64则 变红黑树**,长度小于6变回链表
什么是Hash表?
散列表(Hash Table,也叫哈希表)通过**关键码值(Key Value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找速度,这个映射函数叫做散列函数**,存放记录的数组叫做散列表。hash表里可以存储元素的位置叫做bucket(桶)。 什么是哈希冲突,如何解决?
不同的key值产生了相同的Hash地址,H(key1) == H(key2)解决方案:
开放地址法:不断地探测序列,查找一个空的单元进行插入。有线性探测、再平方、伪随机链地址法:对于相同的hashCode值,使用链表进行连接。使用数组来存储每一个链表。HashMap中使用的方案。公共溢出区法:建立一个特殊的存储空间,专门存储这些冲突的数据,适用数据冲突比较少的情况。再散列法:准备若干个hash函数,如果使用第一个hash函数发生了冲突,就使用第二个hash函数 以此类推。 HashMap源码读过吗? 核心成员变量有哪些知道吗? JDK7中HashMap源码之核心变量
- Entry[] table 这个Entry类型的数组存储了HashMap的真正数据。size大小 代表HashMap内存储了多少个键值对。capacity容量 实际上HashMap没有一个成员叫capacity,它是作为table这个数组的大小而隐式存在的。threshold阈值和loadFactor装载因子 threshold是通过capacity*loadFactor得到的(threshold = capacity * loadFactor)。当size超过threshold时(刚好相等时不会扩容),HashMap扩容会再次计算每个元素的哈希位置。entrySet、KeySet和values这三个都是一种视图,真正的数据来自table
- Node[] table 这个Node类型的数组存储了HashMap的真正数据 static class Node
为什么链表长度为8转为红黑树?
TreeNode(红黑树中)占用空间是普通Node(链表中)的两倍,为了时间和空间的权衡。节点的分布频率会遵循泊松分布,链表长度达到8个元素的概率为0.00000006
为什么链表长度为6,红黑树就转为链表呢?
若是7,则当极端情况下(频繁插入和删除的都是同一个哈希桶)对一个链表长度为8的哈希桶进行频繁的删除和插入,同样也会导致频繁的树化<=>非树化 插入和获取数据的过程清楚吗?
JDK1.7中HashMap插入的过程:— put操作
public V put(K key, V value) {
// 步骤1
if (table == EMPTY_TABLE) { //判断如果表空进行初始化
inflateTable(threshold);
}
// 步骤2
if (key == null) //判断是否键为空
return putForNullKey(value); //遍历table[0]是否存在 e.key == null
// 步骤3
int hash = hash(key); //计算hash值
int i = indexFor(hash, table.length); //计算bucket的位置
for (Entry e = table[i]; e != null; e = e.next) { //遍历链表
Object k;
// hash相同 equals比较是真 则替换 返回旧值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i); //添加数据 这里包含hash冲突的处理。
return null;
}
jdk7 中的hash函数处理
final int hash(Object k) {
int h = hashSeed;
//如果为干扰因子不为0,且传入的key类型为String,则使用特定的算法(sun.misc.Hashing.stringHash32((String) k))对该key进行hash计算。并返回
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//如果哈希干扰因子为0 或者 k的类型不为String则使用异或操作变更key的hashcode
h ^= k.hashCode();
//为了减少Hash冲突出现次数进行必要的位干扰,默认负载因子是8.
// 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);
}
扰动实现复杂,一共九次扰动: 5次异或 加上4次位运算
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//当size大小大于或等于阈值的时候,将会进行扩容操作,将数据元素重新计算位置后放入newTable中,Hash位置会重新计算
resize(2 * table.length); //2倍扩容
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex); //创建Entry的时候会有hash冲突的代码
}
不小于threshold阈值直接2倍扩容,Hash位置会重新计算。
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
//根据头节点构建----头插法的实现
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
Entry(int h, K k, V v, Entryn) { value = v; next = n; key = k; hash = h; }
哈希冲突时链表插入数据使用头插(JDK1.7)
JDK7中获取数据的过程—get操作
//获取key值为key的元素值
public V get(Object key) {
if (key == null)//如果Key值为空,则获取对应的值,这里也可以看到,HashMap允许null的key,其内部针对null的key有特殊的逻辑
return getForNullKey();
Entry entry = getEntry(key);//获取实体
return null == entry ? null : entry.getValue();//判断是否为空,不为空,则获取对应的值
}
//获取key为null的实体
private V getForNullKey() {
if (size == 0) {//如果元素个数为0,则直接返回null
return null;
}
//key为null的元素存储在table的第0个位置
for (Entry e = table[0]; e != null; e = e.next) {
if (e.key == null)//判断是否为null
return e.value;//返回其值
}
return null;
}
get方法通过key值返回对应value,如果key为null,直接去table[0]处检索。
//获取键值为key的元素
final Entry getEntry(Object key) {
if (size == 0) {//元素个数为0
return null;//直接返回null
}
int hash = (key == null) ? 0 : hash(key);//获取key的Hash值
for (Entry e = table[indexFor(hash, table.length)];//根据key和表的长度,定位到Hash桶
e != null;
e = e.next) {//进行遍历
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//判断Hash值和对应的key,合适则返回值
return e;
}
return null;
}
JDK8中HashMap源码插入的过程:
- 首先判断table成员是否初始化,如果没有,则调用resize扩容通过传入键值对的key的hashCode和容量,马上得到了该映射所在的table数组下标。并通过数组的取下标操作,得到该哈希桶的头节点。如果没有发生哈希碰撞(头节点为null),那么直接执行新增操作。如果发生了哈希碰撞(头节点不为null),那么分为两种情况:
- 如果与桶内某个元素==返回True,或者equals判断相同,执行替换操作(图中的直接覆盖value)如果与桶内所有元素判断都不相等,执行新增操作,可能是链表也可能是红黑树的插入。
- 如果哈希桶是单链表结构,且桶内节点数量超过了TREEIFY_THRESHOLD(8),且size大于等于了MIN_TREEIFY_CAPACITY(64),那么将该哈希桶转换成红黑树结构。(图中链表长度是否大于8)链表长度不大于8时,如果新增后size大于了threshold,那么调用resize扩容。
static final int hash(Object key) { // 计算key的hash值
int h;
// 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//进行异或
}
将key的hashCode与该hashCode的无符号右移16位,异或起来得到的。
扰动实现简单 一共2次扰动 1次是异或 加上1次位运算
jdk8中hashmap put方法源码:
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[] tab; Node p; int n, i;
// 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
if ((p = tab[i = (n - 1) & hash]) == null) //头节点是否为空
tab[i] = newNode(hash, key, value, null);
else {
// table表该索引位置不为空,则进行查找
Node e; K k;
// 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeval方法查找目标节点
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeval(this, tab, hash, key, value);
else {
// 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
for (int binCount = 0; ; ++binCount) {
// 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
if ((e = p.next) == null) { //一直遍历到链表的尾部,因为链表的尾部是null
p.next = newNode(hash, key, value, null);
// 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
// 减一是因为循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 将p指向下一个节点
}
}
// 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于linkedHashMap
return oldValue;
}
}
++modCount;
// 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 用于linkedHashMap
return null;
}
JDK8 hashmap中的get方法
- 调用key的hashCode方法,根据返回值定位到map里数组对应的下标判断这个数组下标对应的头节点是不是为null,如果是,返回null如果头节点不是null,判断这个引用对应对象的key值的equals方法,跟查询的key值进行对比,判断是否为true,如果为true则返回这个对象的value值,否则继续遍历下一个节点。如果遍历完map中所有的节点都无法满足上面的判断 则返回null
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value; //计算hash值
}
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
// 1.对table进行校验:table不为空 && table长度大于0 &&
// table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {//判断头节点是否为null
// 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))// always check first node
return first;
// 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
return ((TreeNode)first).getTreeNode(hash, key);
do {
// 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 6.找不到符合的返回空
return null;
}



