美文网首页
基于jdk1.8的HashMap源码分析(一)

基于jdk1.8的HashMap源码分析(一)

作者: 小甲说 | 来源:发表于2018-10-10 22:58 被阅读0次

1.概念

    HashMap是Java中最常用的集合类之一,以<K,V>的形式来进行数据存储。常用来比较的还有HashTable,ConcurrentHashMap 等。

2.数据结构   

        什么是数据结构,在我的认识中,我们的数据需要存储在计算机中,那么它的存储形式,就是一个个的数据结构。常见的数据结构包括:数组,链表,二叉树,红黑树等等

        提起hashMap内部的数据结构 那么大多数开发者都可以说出 在其内部是维护了一个数组,数组的每个元素又是一个单向链表。在1.7之前的版本中,这是没错的,但是维护单向链表由此带来的一个缺点就是当我们的hashMap发生大量的指针碰撞之后,当我们需要get其中的一个值的时候,就有可能需要遍历单向链表,那么时间复杂度就是O(n),为解决这一问题,在1.7之后,hashMap中引入了红黑树,将达到一定条件的链表转变为一个红黑树,此时时间复杂度就变为了O(logn)。

问,为何不使用比如线性探测法 往数组的其他位置放 如果长度不够 则进行扩容

答:第一,这样有可能导致 本来数组还有空余位置就触发了扩容,内存浪费。第二,扩容时 需要根据hash值和数组长度重新计算每一个key的hash值,然后重新排列整个map,消耗系统资源。总体上来说,是一个时间换空间的操作。

3.hashMap如何存储数据

    接下来就是本文的重点了,hashMap到底是如何存储数据的呢?

    让我们带着几个问题来看代码

    (1)数组是什么时候进行初始化的

    (2)数组在内存中为一块连续的内存地址,所以要求我们在初始化数组是设定数组的长度。那么数组的大小设置为多少呢

    (3)hash值如何计算

    (4)什么是hash冲突,发生后冲突后hashMap是如何处理的

public V put(K key,V value) {

return putVal(hash(key), key, value,false,true);

}

这就是在hashMap中进行put的方法,我们需要关注两个点 第一,putVal(hash(key), key, value,false,true) 这是进行存储操作的方法;第二,hash(key)这是计算hash值的方法 。

我们先来看第一个

transient Node[]table;

Node[] tab; Node p;int n, I;     

//声明了一个Node对象,Node元素中包含了如下变量

final int hash;

final K key;

V value;

Nodenext;  //下一个元素的地址引用 佐证了这是一个单向链表

if ((tab =table) ==null || (n = tab.length) ==0)    //此处为判断声明的Node[]是否为空

n = (tab = resize()).length;    //为空执行resize() 方法

在resize()方法中 ,当判断oldTab.length==0时 有如下代码

newCap=DEFAULT_INITIAL_CAPACITY;

newThr = (int)(DEFAULT_LOAD_FACTOR *DEFAULT_INITIAL_CAPACITY);

//下面两行是类中声明的常量

static final int DEFAULT_INITIAL_CAPACITY =1 <<4;// aka 16  位运算 即初始化的数组长度

static final float DEFAULT_LOAD_FACTOR =0.75f;// 负载因子

threshold = newThr;

@SuppressWarnings({"rawtypes","unchecked"})

Node[] newTab = (Node[])new Node[newCap];

table = newTab;

我们可以看到,当首次进行put时,进行了数组的初始化,并定义了数组的长度为16,那么问题来了,数组的长度给定了之后,tab[]下标为0-15。如果我们不断插入数据,当key的hash值不在这16个长度之内时会发生什么呢,让我们继续看源码,还是putval()

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

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

通过这两行代码我们可以看到 去计算了一次hash值 当tab[i]==null时,new了Node元素 放了进去 完成了数据存储

如果此时tab[i]不为null则执行下面的代码。

else {

Node e;K k;

if (p.hash == hash &&

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

e = p;

在这我们看到  当 k的hash值相等且 equals()相等的时候,进行了覆盖

else if (pinstanceof TreeNode)

e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

k不相等的时候 如果当前Node对象包含了红黑树 那么就把它放在了树里

else {

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

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

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

if (binCount >=TREEIFY_THRESHOLD -1)// -1 for 1st 

TREEIFY_THRESHOLD =8

                treeifyBin(tab, hash);

break;

}

对单向链表进行遍历。当p.next==null 是说明这是单向链表的尾结点,指针null。记录单向链表的遍历的次数。 TREEIFY_THRESHOLD=8 当链表长度》=8-1时, 将链表转为红黑树

if (e.hash == hash &&

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

break;

//同样是进行key的hash判断 一致则覆盖掉原有值

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;    //记录put的次数

if (++size >threshold)

resize();

//重点来了 在此时,如果数组需要进行扩容数组在什么时候扩容呢,就是代码中的threshold = newThr

在resize()方法中 如果原有tab长度>0且<2的30次方,则tab长度变为原来的<1 即原来的2倍

size这个值在哪计算还没发现 待我稍后进行补充

afterNodeInsertion(evict);

return null;

4.完整的源码

final V putVal(int hash,K key,V value,boolean onlyIfAbsent,

boolean evict) {

Node[] tab; Node 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 e;K k;

if (p.hash == hash &&

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

e = p;

else if (pinstanceof TreeNode)

e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

else {

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

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

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

if (binCount >=TREEIFY_THRESHOLD -1)// -1 for 1st

                        treeifyBin(tab, hash);

break;

}

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.2.4三个问题 至于第3个问题 改日在叙。如有谬误如疏漏之处烦请各位看官多多指正

相关文章

网友评论

      本文标题:基于jdk1.8的HashMap源码分析(一)

      本文链接:https://www.haomeiwen.com/subject/sbimaftx.html