本文暂时只讨论HashMap中put/get方法原理实现,后续更新扩容机制
本着为读者着想(//不写大白话自己也看不懂)的态度完成
我们用一段简单的代码做测试涵盖
- map处理put/set区分是否为null键两种形式
public static void main(String[] args) {
HashMap<Object, Object> map = new HashMap<>();
map.put(null,null);
//map只关心是不是null键,不关心值这种方式和上面的null键null值方式走一条路
//map.put(null,"xxs");
map.put("key","value");
map.get("key");
map.get(null);
}
前提
在看下面的内容之前,得先知道,map是用数组+链表实现的,我们先不管链表,map怎么判断往数组的哪个位置放键值对呢?
以下是map对于数组下标怎么获取(key!=null的情形),也就是往数组的哪个位置放键值对的代码
//var1 = key
int var3 = this.hash(var1); //获取key的hash
//this.table.length : 该map集合的长度,默认为16
indexFor(var3, this.table.length);
//这里返回的int 就是对应的数组的下标
static int indexFor(int var0, int var1) {
//&运算符 : 先将数字转换为二进制,如果相对应位都是1,则结果为1,否则为0
//为什么要让var1(map长度)减一,因为数组下标是length-1,防越界
return var0 & var1 - 1;
}
通过以上代码可以知道往map里啪啪啪put的时候数据在哪,相反get的时候只要通过同样的方式算出对应的下标就能取出对应的键值对
不知道hash值是啥?
put
一点点介绍,首先是最简单的情况:
null键< map.put(null,null)>
key = var1 / value = var2
//以下为源码部分(只提取出关于null键的)
public V put(K var1, V var2) {//var1=null,var2=null
if (var1 == null) {
return this.putForNullKey(var2);
}
private V putForNullKey(V var1) {
for(HashMap.Entry var2 = this.table[0]; var2 != null; var2 = var2.next) {
if (var2.key == null) {
Object var3 = var2.value;
var2.value = var1;
var2.recordAccess(this);
return var3;
}
}
++this.modCount;
this.addEntry(0, (Object)null, var1, 0);
return null;
}
解释一下,主要是putForNullKey方法
从代码HashMap.Entry var2 this.table[0]; var2 != null;
我们可以看出来,map直接对数组的0号位取值判断是否为空,也就是只要put了null键,就往数组0号位放
var2 = var2.next
这一部分我们放一放下面讲
this.addEntry(0, (Object)null, var1, 0);
储存到数组中的方法
//以下源码
void addEntry(int var1, K var2, V var3, int var4) {
//这一部分是扩容
if (this.size >= this.threshold && null != this.table[var4]) {
this.resize(2 * this.table.length);
var1 = null != var2 ? this.hash(var2) : 0;
var4 = indexFor(var1, this.table.length);
}
//正式添加数据到数组
this.createEntry(var1, var2, var3, var4);
}
----------
void createEntry(int var1, K var2, V var3, int var4) {
//table : map中真正存数据的数组
//Entry 对象 : map中键值对对象,我们存的每对key/value都会被放到这个对象中统一管理
HashMap.Entry var5 = this.table[var4];
//统计大小,调用的HashMap.Size()就是取它,在每次添加元素时++
++this.size;
}
----------
Entry(int var1, K var2, V var3, HashMap.Entry<K, V> var4) {
this.value = var3; //值
this.next = var4; //链表指向
this.key = var2; //键
this.hash = var1; //key的hash值
}
- 非null键< map.put(“key”,“value”)>
//以下源码
var1 = "key" / var2="value"
public V put(K var1, V var2) {
if (var1 == null) {
//null键 上文解析
return this.putForNullKey(var2);
} else {
//非null键
//获取key的hash值
int var3 = this.hash(var1);
//根据hash获取应该放到数组的第几位(上文解析该方法)
int var4 = indexFor(var3, this.table.length); //var4 角标
//【关键】 下文详解
for(HashMap.Entry var5 = this.table[var4]; var5 != null; var5 = var5.next) {
if (var5.hash == var3) {
Object var6 = var5.key;
if (var5.key == var1 || var1.equals(var6)) {
Object var7 = var5.value;
var5.value = var2;
var5.recordAccess(this);
return var7;
}
}
}
++this.modCount;
this.addEntry(var3, var1, var2, var4);
return null;
}
}
在了解map储存非null键之前,要先解决一个问题:
数组初始化大小为16,如果储存16个key在通过
indexFor
方法获取的索引下标都相同怎么办?
for(HashMap.Entry var5 = this.table[var4]; var5 != null; var5 = var5.next) {
if (var5.hash == var3) {
Object var6 = var5.key;
if (var5.key == var1 || var1.equals(var6)) {
Object var7 = var5.value;
var5.value = var2;
var5.recordAccess(this);
return var7;
}
}
}
this.addEntry(var3, var1, var2, var4);
上面这段循环解决了角标相同时的链表储存形式
循环首先判断需要放置数据的坑是否有值(HashMap.Entry var5 = this.table[var4]; var5 != null;
如果为空不进循环,按照正常插入数据
如果不为空,即为有值,判断该值的key与新插入数据的key的hash是否一致(从hash判断是否相同值,理论可能出现hash相同值不同情况)if (var5.hash == var3)
,如果true则继续判断其地址/值是否相同if (var5.key == var1 || var1.equals(var6))
如果相同覆盖该值。
若第一次循环的值不相同,找下一个(Entry.next(上图箭头))直到下一个为空(没有指向)
循环结束,执行添加数据的方法
//var1=key,var2=value,var3=key.hash(),var4=数组角标
void addEntry(int var1, K var2, V var3, int var4) {
...
this.createEntry(var1, var2, var3, var4);
}
void createEntry(int var1, K var2, V var3, int var4) {
//这里将在这个坑的原有数据取出
//var5=原数据
HashMap.Entry var5 = this.table[var4];
this.table[var4] = new HashMap.Entry(var1, var2, var3, var5);
++this.size;
}
----------
//将新键值加入到原有坑,旧的值连同它后面的链表都防止新键值的next
Entry(int var1, K var2, V var3, HashMap.Entry<K, V> var4) {
this.value = var3; //值
this.next = var4; //链表指向
this.key = var2; //键
this.hash = var1; //key的hash值
}
加入新的键值对是从前进入,可以理解为类似栈结构。
get
同样分为是否取null的值
<map.get(null);
>
//取值源码
private V getForNullKey() {
for(HashMap.Entry var1 = this.table[0]; var1 != null; var1 = var1.next) {
if (var1.key == null) {
return var1.value;
}
}
找到数组0号元素,迭代后面的链表,找到key=null,取值即可
<map.get("key");
>
public V get(Object var1) {
if (var1 == null) {
return this.getForNullKey();
} else {
HashMap.Entry var2 = this.getEntry(var1);
return null == var2 ? null : var2.getValue();
}
}
final HashMap.Entry<K, V> getEntry(Object var1) {
//再次判断key是否为null
int var2 = var1 == null ? 0 : this.hash(var1);
for(HashMap.Entry var3 = this.table[indexFor(var2, this.table.length)]; var3 != null; var3 = var3.next) {
if (var3.hash == var2) {
Object var4 = var3.key;
if (var3.key == var1 || var1 != null && var1.equals(var4)) {
return var3;
}
}
}
return null;
}
整体和put相似,先根据key算出hash,根据indexFor方法计算角标,迭代链表,如果hash值相同进一步判断是否是同一对象,如果也相同认为是相同对象,返回该键值对
吃饭去了