Java HashMap 源码解析 (一)

数据结构:
首先,回顾ArrayList和LinkedList
底层实现分别是数组和链表:
数组查询快,因为数组查询是由数组下标,并且存储在内存是连续存储的,要删除一个数据,那他后面的数据都需要向前 移位
链表增删快,是因为链表物理存储上是可以是离散的,它里面有一个地方专门存储next,指向下一个数据,直接 修改指向即可。


HashMap的大致存储结构
下面我们看一下HashMap在进行存数据的时候都发生了什么。
HashMap<Integer,String> map1 = new HashMap<>();
map1.put(2, "map1");
用这段小代码为例。
一个小小的put 函数
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
显然他去调用了putVal(*),然而注意到,在putVal的第一个参数,是hash()函数,也就是说他并没有简单使用hashCode直接对key进行计算,而是用了如下方法:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
    1. 对于key进行hashcode运算赋个变量h,
    2. 然后对于h这个值进行无符号右移16位,
    3. 最后将h 和 h右移16位之后的值进行异或运算。
第一步,对key调用自身对象的hashCode函数,由于我们测试使用的是Int型,那么他会调用Integer的方法。
对于key的hashCode调用,返回的是Integer重写的hashCode,也就是直接返回值。
public int hashCode() {
    return Integer.hashCode(value);
}
public static int hashCode(int value) {
    return value;
}
那么h=2
第二步,对h无符号右移16位. 
无符号右移两步走:跟正数右移一样。
1. 写出原码                          0010
2. 右移n位算10进制。    0000..别说16位,2位就是0,所以移后的0.
第三步,计算h 和h右移后的异或
异或就是相同为0,不同为1
0010异或
0000 =0010. 还是2,所以这个hash(key)函数在传入int2之后,还是    hash (key)

hash函数的具体实现
主要是上面这一句,key是否等于null,如果不等于空,那么就执行一个hashcode运算赋值给临时变量h,
然后让h向右位移16位。然后对左右这两个值取异或!
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//这里打断点进入
//得到的key的值是class java.lang.invoke.MethodHandle ,这明显不是我们put的int 类型,那这是什么呢?
其实这些都是java程序在每次启动时候进行的环境加载
java 程序启动的收,会用类 ProcessEnvoriment.java 他继承自HashMap,
在其静态代码块中,来对java所有的 环境参数使用hashmap进行存储。包括PATH、JDK、 username等等很多参数。
并且利用HashMap,java会去加载很多jar包,包括ext下面的。
现在,来正常的put一个数据看看hashMap怎么工作的:put一个key=2, value=“map”的数据。
2的hashCode计算还是2
2的无符号右移16位得到0,当然那么小
2 按位异或 0 之后还是 2, 因为二进制 10 异或 00 还是 10, 异或的定义就是相同为0,不同为1.
//现在开始putVal,初始化属性。
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((p = tab[i = (n - 1) & hash]) == null) //判断,如果p是空,那就进入下一步,讲对象进行存储。
//n-1=15 , hash =2, tab[n]是一个长度为16 的数组,存储Node链表对象。 现在要做的就是用(n-1) 和 hash 进行按位与。
与运算:
0000 1111 和
0000 0010 进行与运算, 有0得0
0000 0010 ,计算=2
那么将存该数据到数组的[2]号位。 tab[2] = "map"
tab[i] = newNode(hash, key, value, next:null);// 进行链表的存储
//那么考虑, 如果计算得到p的位置不是null呢?那我们再去存一个key=2的数据进来即可看到,
//我们目前已经存入一个key=2的元素了:value=map1
//由于tab[2]位置已经有一个数据了,那么他会进入else进行进一步判断
else {
    Node<K,V> e; K k;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))

//p长这样:就是我们想要放入的元素,然后将p付给e,Node类型(链表元素)。
if (e != null) { // existing mapping for key
    V oldValue = e.value;                     //把老的数据赋值给一个临时变量
    if (!onlyIfAbsent || oldValue == null)    //判断如果!onlyIfAbsent 是真,现在是的
        e.value = value;                      //那么把e的value改写成 最新的value
    afterNodeAccess(e);                       //LinkedHashMap使用的,这里先忽略
    return oldValue;                          //将老的值返回,如果需要的话。

hash碰撞出现的情景?
(1)一般会出现在大的数据情况之下
(2)hashcode的生成方法唯一性较弱(比如人为的生产hashcode)


怎么保证上面这样算出来的hash值能够数组不越界呢?
if ((tab = table) == null || (n = tab.length) == 0)    // 进行数组大小初始化
    n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)             // 进行数组key值计算,然后进行与运算。保证数组绝对不会越界
    tab[i] = newNode(hash, key, value, null);

java的位移运算:传送门

为什么这里定义数组的初始化大小是16,但不直接写16,要写成1往左移4位: 原因是  快!
/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


还有个小问题:在上面的操作中,为什么要把h右移16位,又为什么在进行异或操作?
加入map的key不是int,而是String “nakumalatat” 进行hashcode运算得“-1695884160”
再右移16位得“39658”, 进行异或运算得-1695922582

HashMap里面最重要的内部类是Node<K,V>,从它内部属性即可看出,这就是我们前面提到的链表的实现。
static class Node<K,V> implements Map.Entry<K,V>
链表内部也印证了猜测,除了hash这个属性以外,其他属性都一目了然。
final int hash;
final K key;
V value;
Node<K,V> next;
为什么不直接hashCode
hash 属性就是为了弥补hashCode计算key值过后,无法保证元素一定数组不越界而诞生的。

在判断是新的map后, 会进入resize 函数,进行初始化工作
final Node<K,V>[] resize() 
给一个初始化的默认长度为16.
newCap = DEFAULT_INITIAL_CAPACITY;
并且立即计算阈值,为0.75 * 16 =12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
然后返回putVal进行位置计算,那么n 是之前的到的初始化数组长度=16, hash是传入的key计算结果
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
tab[i = (n -  1 ) & hash] 用来计算位置
15 & hash 15与hash,与运算,相信大家都知道怎么算, 与: 遇0 得0
所以 -1695922582 前面一大堆数字都被0消掉了,
即使是负数,那么先找他的补码,但前面依然会被0给消掉。
只剩下15对应的那几位了,就不算了,太长了。

HashMap的大致存储结构
下面我们看一下HashMap在进行存数据的时候都发生了什么。
HashMap<Integer,String> map1 = new HashMap<>();
map1.put(2, "map1");
用这段小代码为例。
一个小小的put 函数
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
显然他去调用了putVal(*),然而注意到,在putVal的第一个参数,是hash()函数,也就是说他并没有简单使用hashCode直接对key进行计算,而是用了如下方法:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
    1. 对于key进行hashcode运算赋个变量h,
    2. 然后对于h这个值进行无符号右移16位,
    3. 最后将h 和 h右移16位之后的值进行异或运算。
第一步,对key调用自身对象的hashCode函数,由于我们测试使用的是Int型,那么他会调用Integer的方法。
对于key的hashCode调用,返回的是Integer重写的hashCode,也就是直接返回值。
public int hashCode() {
    return Integer.hashCode(value);
}
public static int hashCode(int value) {
    return value;
}
那么h=2
第二步,对h无符号右移16位. 
无符号右移两步走:跟正数右移一样。
1. 写出原码                          0010
2. 右移n位算10进制。    0000..别说16位,2位就是0,所以移后的0.
第三步,计算h 和h右移后的异或
异或就是相同为0,不同为1
0010异或
0000 =0010. 还是2,所以这个hash(key)函数在传入int2之后,还是    hash (key)

hash函数的具体实现
主要是上面这一句,key是否等于null,如果不等于空,那么就执行一个hashcode运算赋值给临时变量h,
然后让h向右位移16位。然后对左右这两个值取异或!
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//这里打断点进入
//得到的key的值是class java.lang.invoke.MethodHandle ,这明显不是我们put的int 类型,那这是什么呢?
其实这些都是java程序在每次启动时候进行的环境加载
java 程序启动的收,会用类 ProcessEnvoriment.java 他继承自HashMap,
在其静态代码块中,来对java所有的 环境参数使用hashmap进行存储。包括PATH、JDK、 username等等很多参数。
并且利用HashMap,java会去加载很多jar包,包括ext下面的。
现在,来正常的put一个数据看看hashMap怎么工作的:put一个key=2, value=“map”的数据。
2的hashCode计算还是2
2的无符号右移16位得到0,当然那么小
2 按位异或 0 之后还是 2, 因为二进制 10 异或 00 还是 10, 异或的定义就是相同为0,不同为1.
//现在开始putVal,初始化属性。
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((p = tab[i = (n - 1) & hash]) == null) //判断,如果p是空,那就进入下一步,讲对象进行存储。
//n-1=15 , hash =2, tab[n]是一个长度为16 的数组,存储Node链表对象。 现在要做的就是用(n-1) 和 hash 进行按位与。
与运算:
0000 1111 和
0000 0010 进行与运算, 有0得0
0000 0010 ,计算=2
那么将存该数据到数组的[2]号位。 tab[2] = "map"
tab[i] = newNode(hash, key, value, next:null);// 进行链表的存储
//那么考虑, 如果计算得到p的位置不是null呢?那我们再去存一个key=2的数据进来即可看到,
//我们目前已经存入一个key=2的元素了:value=map1
//由于tab[2]位置已经有一个数据了,那么他会进入else进行进一步判断
else {
    Node<K,V> e; K k;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))

//p长这样:就是我们想要放入的元素,然后将p付给e,Node类型(链表元素)。
if (e != null) { // existing mapping for key
    V oldValue = e.value;                     //把老的数据赋值给一个临时变量
    if (!onlyIfAbsent || oldValue == null)    //判断如果!onlyIfAbsent 是真,现在是的
        e.value = value;                      //那么把e的value改写成 最新的value
    afterNodeAccess(e);                       //LinkedHashMap使用的,这里先忽略
    return oldValue;                          //将老的值返回,如果需要的话。

hash碰撞出现的情景?
(1)一般会出现在大的数据情况之下
(2)hashcode的生成方法唯一性较弱(比如人为的生产hashcode)


怎么保证上面这样算出来的hash值能够数组不越界呢?
if ((tab = table) == null || (n = tab.length) == 0)    // 进行数组大小初始化
    n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)             // 进行数组key值计算,然后进行与运算。保证数组绝对不会越界
    tab[i] = newNode(hash, key, value, null);

java的位移运算:传送门

为什么这里定义数组的初始化大小是16,但不直接写16,要写成1往左移4位: 原因是  快!
/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


还有个小问题:在上面的操作中,为什么要把h右移16位,又为什么在进行异或操作?
加入map的key不是int,而是String “nakumalatat” 进行hashcode运算得“-1695884160”
再右移16位得“39658”, 进行异或运算得-1695922582

HashMap里面最重要的内部类是Node<K,V>,从它内部属性即可看出,这就是我们前面提到的链表的实现。
static class Node<K,V> implements Map.Entry<K,V>
链表内部也印证了猜测,除了hash这个属性以外,其他属性都一目了然。
final int hash;
final K key;
V value;
Node<K,V> next;
为什么不直接hashCode
hash 属性就是为了弥补hashCode计算key值过后,无法保证元素一定数组不越界而诞生的。

在判断是新的map后, 会进入resize 函数,进行初始化工作
final Node<K,V>[] resize() 
给一个初始化的默认长度为16.
newCap = DEFAULT_INITIAL_CAPACITY;
并且立即计算阈值,为0.75 * 16 =12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
然后返回putVal进行位置计算,那么n 是之前的到的初始化数组长度=16, hash是传入的key计算结果
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
tab[i = (n -  1 ) & hash] 用来计算位置
15 & hash 15与hash,与运算,相信大家都知道怎么算, 与: 遇0 得0
所以 -1695922582 前面一大堆数字都被0消掉了,
即使是负数,那么先找他的补码,但前面依然会被0给消掉。
只剩下15对应的那几位了,就不算了,太长了。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值