HashMap的实现原理

return true;

}

return false;

}

方法

=================================================================

get方法


返回指定键映射到的值,如果此映射不包含键的映射,则返回 null

public V get (Object key){

//定义一个Node

Node<K, V> e;

//如果有这个结点,就返回这个结点对应的值,否则返回null

return (e = getNode(hash(key), key)) == null ? null: e.value;

}

final Node<K, V> getNode ( int hash, Object key){

Node<K, V>[] tab;

Node<K, V> first, e;

int n;

K k;

//如果哈希表不为空 && key 对应的桶上不为空

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 && ke

y.equals(k))))

return first;

//判断是否有后续节点

if ((e = first.next) != null) {

//如果当前的桶是采用红黑树处理冲突,则调用红黑树的 get 方法去获取节点

if (first instanceof TreeNode)

return ((TreeNode<K, V>) first).getTreeNode

(hash, key);

//不是红黑树的话,那就是传统的链式结构了,通过循环的方法判断链中是否存在该 key

do {

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

return e;

} while ((e = e.next) != null);

}

}

return null;

}

实现步骤大致如下:

  1. 通过 hash 值获取该 key 映射到的桶。

  2. 桶上的 key 就是要查找的 key,则直接命中。

  3. 桶上的 key 不是要查找的 key,则查看后续节点:

1)如果后续节点是树节点,通过调用树的方法查找该 key。

2)如果后续节点是链式节点,则通过循环遍历链查找该 key。

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<K, V>[] tab;

Node<K, V> p;

int n, i;

//如果哈希表为空,则先创建一个哈希表

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

//如果当前桶没有碰撞冲突,则直接把键值对插入,完事

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

else {

Node<K, V> e;

K k;

//如果桶上节点的 key 与当前 key 重复,那你就是我要找的节点了

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

//如果是采用红黑树的方式处理冲突,则通过红黑树的 putTreeVal 方法去插入这个键值对

else if (p instanceof TreeNode)

e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);

//否则就是传统的链式结构

else {

//采用循环遍历的方式,判断链中是否有重复的 key

for (int binCount = 0; ; ++binCount) {

//到了链尾还没找到重复的 key,则说明 HashMap 没有包含该键

if ((e = p.next) == null) {

//创建一个新节点插入到尾部

p.next = newNode(hash, key, value, null);

//如果链的长度大于 TREEIFY_THRESHOLD 这个临界值,则把链变为红黑树

if (binCount >= TREEIFY_THRESHOLD - 1)

// -1 for 1st

treeifyBin(tab, hash);

break;

}

//找到了重复的 key

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;

}

}

//这里表示在上面的操作中找到了重复的键,所以这里把该键的值替换为新值

if (e != null) { // existing mapping for key

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null)

e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

++modCount;

//判断是否需要进行扩容

if (++size > threshold)

resize();

afterNodeInsertion(evict);

return null;

}

实现步骤大致如下:

  1. 先通过 hash 值计算出 key 映射到哪个桶。

  2. 如果桶上没有碰撞冲突,则直接插入。

  3. 如果出现碰撞冲突了,则需要处理冲突:

1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入。

2)否则采用传统的链式方法插入。如果链的长度到达临界值并且桶的容量大于64,则把链转变为红黑树

  1. 如果桶中存在重复的键,则为该键替换新值。

  2. 如果 size 大于阈值,则进行扩容。

remove方法


参数仅为key时从该地图中删除指定键的映射(如果存在)。

参数为键值对时仅当指定的密钥当前映射到指定的值时删除该条目。

public V remove(Object key) {

Node<K,V> e;

return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;

}

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;

//如果当前 key 映射到的桶不为空

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;

//如果桶上的节点就是要找的 key,则直接命中

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);

}

}

//比对找到的 key 的 value 跟要删除的是否匹配

if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {

//通过调用红黑树的方法来删除节点

if (node instanceof TreeNode)

((TreeNode<K,V>)node).removeTreeNode(this, t ab, movable);

//使用链表的操作来删除节点

else if (node == p)

tab[index] = node.next;

else

p.next = node.next;

++modCount;

–size;

afterNodeRemoval(node);

return node;

}

}

return null;

}

删除操作和添加操作类似

hash方法


在 get 方法和 put 方法中都需要先计算 key 映射到哪个桶上,然后才进行之后的操作,

计算的主要代码如下:

计算得到的值肯定位于0-n之间,保证了访问数据的合法性

n 指的是哈希表的大小,hash 指的是 key 的哈希值

(n - 1) & hash

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16);

从代码可以看到通过异或key的hashcode高16位和低16位得到hash.

这个 hash 方法先通过 key 的 hashCode 方法获取一个哈希值,再拿这个哈希值与它的高 16 位的哈希值做一个异或操作来得到最后的哈希值,计算过程可以参考下图。为啥要这样做呢?注释中是这样解释的:如果当 n 很小,假设为 64 的话,那么 n-1即为 63(0x111111),这样的值跟 hashCode()直接做与操作,实际上只使用了哈希值的后 6 位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成冲突了,所以这里把高低位都利用起来,从而解决了这个问题。

正是因为与的这个操作,决定了 HashMap 的大小只能是 2 的幂次方,即使你在创建 HashMap 的时候指定了初始大小,HashMap 在构建的时候也会调用下面这个方法来调整大小:

因为当n的大小位2的幂次方时,n-1的最后一位总是位1,这样参与计算&运算才有意义,如果位0,则末尾总是为0.

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; }

resize方法


在java8中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) {

//如果当前容量超过最大容量,则无法进行扩容

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 signifiesusing defaults

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);

}

//新的 resize 阈值

threshold = newThr;

//创建新的哈希表

@SuppressWarnings({“rawtypes”,“unchecked”})

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

table = newTab;

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;

}

这里需要注意的是:

扩容的阈值并不是桶占用的个数超过阈值时就扩容,而是当键值对的个数超过阈值时就会进行扩容。

总结

=================================================================

哈希表的初始容量为16,默认负载因子为0.75,当键值对的个数超过阈值时就会进行扩容,扩容为原来的两倍,当一个桶中的链长度超过8,并且表的容量大于64时就会转换为红黑树,当小于6时会由红黑树重新转换为链式存储

加载因子为什么默认值为0.75f?

  • 当加载因子过大时,扩容阈值也变大,也就是说扩容的门槛提高了,这样容量的占用就会降低。但这时哈希碰撞的几率就会增加,效率下降;

  • 当加载因子过小时,扩容阈值变小,扩容门槛降低,容量占用变大。这时候哈希碰撞的几率下降,效率提高。

加载因子是基于容量和性能之间平衡的结果,容量占用和性能是此消彼长的关系,它们的平衡点由加载因子决定,0.75是一个即兼顾容量又兼顾性能的经验值。

HashMap如何实现序列化和反序列化

HashMap通过自定义的readObject/writeObject方法自定义序列化和反序列化操作。这样做主要是出于以下两点考虑:

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
此消彼长的关系,它们的平衡点由加载因子决定,0.75是一个即兼顾容量又兼顾性能的经验值。

HashMap如何实现序列化和反序列化

HashMap通过自定义的readObject/writeObject方法自定义序列化和反序列化操作。这样做主要是出于以下两点考虑:

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-EYkCrCQD-1715689735028)]

[外链图片转存中…(img-OTqUTP3T-1715689735029)]

[外链图片转存中…(img-drqzxtFm-1715689735030)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值