java map得put方法源码_哪位大神给解读一下jdk里面Map中put方法的一段源代码?

本文深入探讨了HashMap的工作原理,包括其核心方法put和get的具体实现,以及如何通过hashCode和equals方法确保高效的数据存储与检索。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap原理分析

HashMap最重要的两个方法就是:(这里先不考虑泛型)

put(Object key, Object value);

Object get(Object key);对于put方法,是这样描述的:如果key已存在就更新其value,如果key不存在就添加key和value。

对于get方法,是这样描述的:如果key已存在就返回其value,如果key不存在就返回null。

更关键的是对于这两个方法,其时间复杂度应该近似于O(1),而不能是O(N)。

那么问题一:如何判断key存在?Java的规定是调用下面这个方法,如果能有一个已存在的key返回 true,那么表示这个key就已存在。

boolean equals(Object obj);这个方法是Object类的方法,所以任何对象都有,但是显然其默认实现可能并不符合我们的业务逻辑,有时需要覆盖其默认实现。

那么问题二:hashMap如何存储数据呢?使用链表,使用数组,使用二叉树?

a) 使用链表,那么时间复杂度显示是O(N)不符合要求。

b) 使用二叉树,如果是一颗平衡的树,最佳复杂度为O(log2N),如果是极端不平衡的树,最差复杂度为O(N),也不符合要求。

c) 使用数组,如果是遍历迭代那么和链表没有区别,除非我能从 Key 直接定位到某个确定下标,才有可能实现O(1)的时间复杂度。

那么问题三:如何从Key获取到固定的数组下标?

数组下标是个整数,Key的类型可能多种多样,怎么转换?答案:借助 hashCode() 方法。

hashCode的取值范围是整个Int的取值范围,数组下标的取值范围是 0 到 arr.length - 1,所以还需要借助一个方法,来将hashCode映射到数组下标的取值范围中去,就是 indexFor() 方法。

int hashCode();

int indexFor(int hash, int length);

那么问题四:数组下标有了,原位置可能没有数据,有一个数据,有多个数据,如何处理?

a) 如果原位置没有,那么将key和value组成键值对直接方法该位置即可。(添加数据)

b) 如果原位置数据,且只有一个,那么比较key是否相等,如果相等,更新数据。

如果不相等,添加数据。判断方法为:先比较其hashCode,如果hashCode不相等,则直接判定不相等,如果hashCode相等,则再调用equals方法,根据其返回值判定是否相等。

c) 如果是更新数据,不用多提,只要更新键值对的 value 即可,如果是添加数据,且原位置已经有数据的话怎么办呢?答案是组成链表再放在该位置。(JDK8已经变更了实现,当同一位置的元素达到8个时,会触发红黑树化操作,将链表改为红黑树,当元素数量降到6个时,会触发去红黑树化操作,将红黑树改为链表。)

d) 这里同时引出了一个问题,就是判断 Key 是否存在时也需要沿着链表一个一个判断,全部判断完毕之后才确定的说这个 Key 不存在,可以添加数据了。

那么问题五:既然有可能两个 Key 映射到一个位置,还要组成链表,那么还能说是O(1)的时间复杂度吗?

这就牵扯到“碰撞”几率的问题。两个 Key 映射到一个位置,称之为发生碰撞,显然数组越大,碰撞几率越小,元素添加的越多,碰撞几率越大,这个有个比例,即已有元素 除以 数组大小,假设为P。根据经验如果P>0.75,那么碰撞几率就很大了,HashMap的性能有一定影响,如果P<0.5,空间又比较浪费,JDK8以前的一个策略是,如果P超过了0.75,就将底层数组扩大一倍的容量。这个P的阀值是程序员可以根据需要自己指定的。

显然如果完全没有碰撞,那么就是O(1)的复杂度,显然没有这么乐观,如果碰撞不严重的话还是可以期待其有较高的性能的(这就要求其 Key 的hashCode方法必须编写好),就算有一些碰撞一般也认为其时间复杂度应该在O(logN)之下的。

那么问题六:既然上面频繁用到了equals方法和hashCode方法,那么这两个方法的编写有没有什么要求?

如果你是使用JDK类库的类作为Key,如String,Integer那么不必考虑覆盖这两个方法,都是已经编写好了的。如果是自定义类,但是业务逻辑上的判断依据也是某个JDK的类,那么编写方法也很简单,如这个User类。

public class User {

private String username;

private String password;

private String email;

@Override

public int hashCode() {

HashCodeBuilder builder = new HashCodeBuilder();

builder.append(username);

return builder.toHashCode();

}

@Override

public boolean equals(Object obj) {

if (obj == this) {

return true;

}

if (obj == null || obj.getClass() != this.getClass()) {

return false;

}

User other = (User) obj;

EqualsBuilder builder = new EqualsBuilder();

builder.append(this.username, other.username);

return builder.isEquals();

}

}假设你的业务逻辑要求是根据 username 判断整个User是否相等,那么就可以像上面那样编写。

如果你的业务逻辑比较复杂,那么恐怕就不能简单的编写了,只能列出几个原则:

①对于两个相等的对象,其 equals 必须返回 ture,其 hashCode 必须相等。

②对于两个不等的对象,其 equals 必须返回 false,其 hashCode 尽量不相等,但不强求。

③对于有规律变化的对象,其 hashCode 如果能返回无规律的变化,则说明混淆度高,一般来说混淆度越高越好(但不强求)。一个 int 是32个二进制位,如果这种变化能够遍及所有的位,则更好(也不强求)。

PS:indexFor函数,如果能将 hashCode高低位的变化,无规律的给出下标值,那么说明其混淆度够高,也是越高越好,但这个函数是JDK已经写好了的,所以我们不用处理。

那么问题来了,这段代码到底怎么解读:

public V put(K key, V value) {

// 如果table是空talbe,就初始化之,不多解释。

if (table == EMPTY_TABLE) {

inflateTable(threshold);

}

// 如果key是null,就XXX,不是重点,不多解释。

if (key == null)

return putForNullKey(value);

// 获取hash值

int hash = hash(key);

// 获取下标值

int i = indexFor(hash, table.length);

// table就是内部数组,table[i]就是位置。

// 显然如果一开始位置为null,就直接进入添加数据的流程即可。

for (Entry e = table[i]; e != null; e = e.next) {

Object k;

// e.hash 实际上就是e.key.hashCode(),是之前添加时缓存好的。

// 如果 hash 不一样,显然就不用接着判断了,可以直接取链表下一个数据了。

// 如果 hash 一样,就可以判断 equals 了,这里先用 == 比较了,是一种快速判断的技巧,如果去掉也不影响结果,只是可能比较耗时。

if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

// 显然这里说明找到了 key, 要执行更新数据的操作。

V oldValue = e.value;

e.value = value;

e.recordAccess(this);

return oldValue;

}

}

// 如果运行到这里,说明没找到 key,需要执行添加数据的操作。

modCount++;

// 这个怎么添加也有一点小技巧,就是考虑到最近添加往往可能更常用,所以将最近添加的放到链表前部,已有数据放到后部,具体请自行看代码吧。

addEntry(hash, key, value, i);

return null;

}

显然,只有发生碰撞的情况下,才会真的执行for循环,否则的话也只需要计算一个hash,调用一个indexFor,然后比较hash,调用equals方法即可。显然时间花费为常数时间,是O(1)的。

如果极限碰撞的情况下,如所有的 Key 都映射到一个位置上了,其复杂度也会退化到O(N),但除非是故意让 hashCode 返回一样的值,否则不太可能出现这种情况。

少量碰撞的情况下,平均复杂度应该在O(1)和O(logN)之间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值