java底层源码学习001 之 HashMap

HashMap的数据结构

HashMap是以数组加链表或树的方式实现:
一个简单的HashMap可能的存储结构:
这里写图片描述

常量

一. HashMap的常量
//默认的初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//默认的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当某一列(桶)数据不少于8的时候,该列(桶)就会以二叉树的形式存储,如上图的d1~d9
static final int TREEIFY_THRESHOLD = 8;
//当某个桶数据不超过6的时候,该桶就会以链表形式存储,如b1~b4
static final int UNTREEIFY_THRESHOLD = 6;
//暂时没搞懂
static final int MIN_TREEIFY_CAPACITY = 64;

二. 数据结构

结点

1.结点元素结构

//链表结构的Entry
static class Node<K,V> implements Map.Entry<K,V>
//树结点结构的Entry
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>

构造:

HashMap底层是以数组加链表或者树构成,Node<K,V>[] table就是它的数组,table[0]存储着a链表,只不过这个链表只有一个头结点a,table[1]则存储着b链表,table[3]存储着d树
Node类里的属性有:

        final int hash;
        final K key;//键
        V value;//值
        Node<K,V> next;//下一个结点

由于a没有下一个元素,所以a.next=null
而b1.next=b2 ;b2.next=b3…

Node类有一个子类为TreeNode,采用的是红黑树结点结构:

        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;

d1.left=d2….

属性

2.HashMap对象属性:

transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;

HashMap原理

下图右上角的第一句文字有个不准确的地方:
修正: 对HashMap执行删除,增加,清空,一切对size有影响的操作
比如,map.remove(o),map.put(k,v)(之前map中没有k),map.clear()等等
反例: map.put(k,v),如果map之前存了k,那么modCount的值是不会修改的
这里写图片描述



HashMap的重要方法

三. HashMap的重点方法

构造方法

1.构造函数和描述

HashMap()
构造一个具有默认初始容量(16)和默认负载因子(0.75)的空HashMap。

HashMap(int initialCapacity)
构造具有指定初始容量和默认负载因子(0.75)的空HashMap。

HashMap(int initialCapacity, float loadFactor)
构造具有指定的初始容量和负载因子的空HashMap。

HashMap(Map<? extends K,? extends V> m)
构造与指定Map相同映射的新HashMap。


扩容resize()

2.resize扩容:

1) 计算新的临界值newThr和扩容数组大小newCap
这里写图片描述
说白了就是:
只要保证在规定的数值范围内:
1)) 如果table初始化过了,只要table里的容量没有达到最大值,就按照一倍扩容,newThr=oldThr左移一位,newCap=oldCap左移一位;也就是说新的threshold和table大小都乘了2.
2)) 如果table没有初始化,但是之前构造的时候指定了threshold的值(比如:HashMap(int initialCapacity, float loadFactor),这个构造函数就会有根据参数计算出threshold),按照指定的threshold和loadFactor计算新的大小,规则是: newCap=threshold; newThr=newCap*loadFactor
3)) 如果既没有初始化,也没有指定,就按照默认方案赋值,newCap=16,newThr=16*0.75

2) 构建新的table:

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//将oldTab拷贝到新的newTab中
...

这是HashMap拷贝过程中的源码:

if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {//遍历oldTab
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {
            oldTab[j] = null;//将引用设为null,提高垃圾收回性能,童鞋学到了吗?
            if (e.next == null)//如果e是单个值
                newTab[e.hash & (newCap - 1)] = e;//相当于e.hash%newCap取模,同学们,你学到了吗?
            else if (e instanceof TreeNode)//如果e为树
            //交给TreeNode里的split方法将整棵树oldCap[j],拷贝到newTab中
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
             //如果是链表,就按下面的方式拷贝oldCap[j]
            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;
                    //e.hash & oldCap相当于(e.hash/oldCap)%2,童鞋们,学到了吗?
                    //思考: 这里为什么要将e链表的结点分为两条链表loHead和hiHead?
                    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;
                    //将loHead链表存入newTab[j]
                    newTab[j] = loHead;
                }
                if (hiTail != null) {
                    hiTail.next = null;
                    //将hiHead链表存入newTab[j+oldCap]
                    newTab[j + oldCap] = hiHead;
                }
            }
        }
    }
}

思考: 为什么要将e链表的结点分为两条链表loHead和hiHead?然后直接将
newTab[j] = loHead;
newTab[j + oldCap] = hiHead;
就完成了拷贝过程?

解释:
扩容案例:
这里写图片描述

首先计算出threshold=newThr=24,
table = newTab = new Node[newCap=32]

以b1~bn为例:
当遍历oldTab到1时:
b1~bn链表存储在oldTab[1]中,也就是b1~bn的hash值对16取模为1,如:
1,17,33,49,65,81,97,113,...,16*n+1
HashMap扩容最精妙的地方,就是以两倍的方式扩容,这在1)的1))里面分析过了,也就是:newTab.length = 2*oldTab.length
从b1~bn所有的hash值,对32取模就只有两类:
模为1的,即(hash/16)%2==0: 1,33,65,97...
模为16+1=17的,即(hash/16)%2==1: 17,49,81,131...
分为两类链表后,即可将链表的头结点,直接给数组引用:
newTab[1] = loHead; 
newTab[17] = hiHead;


思考:当线程A正在执行resize操作,线程B也开始了resize操作,两个线程执行完毕之后,HashMap的table会怎么样?

答: table的一部分数据可能会丢失,这取决于B线程和A线程的执行拷贝速度
解释:
仔细分析resize的步骤:

//0. 初始化一些变量,如oldTab
oldTab = table
//1. 计算threshold和newCap大小
...
//2. 创建newTab,并将table指向newTab
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//3. 将oldTab拷贝到新的newTab中
...

这里写图片描述


put,putAll,putMapEntries的底层putVal()

3.putVal添加一个新元素:

源码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict){
//tab对table的引用,p构建要添加的结点,n当前table.length,i变量备用
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果table还没有初始化,resize初始化一下
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
//判断table[hash%n]是否已经有结点了,如果没有,就将新添加的结点添加
//(n-1)&hash等价于hash%n取模
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
//否则,说明当前的table[hash%n]已经有结点了,应该将要添加的值,插入该链表或树当中
else {
    //e要构建要添加的结点
    Node<K,V> e; K k;
    /*以下的if ,else if,else 就是为了构建e*/
    //添加的值的key和当前的结点的key,hash值相等,并且两者相等
    //此处相当于: map.put("123",124);map.put("123",123);
    //第二条语句执行时,一定会执行if判断,并且执行e=p,p的key为"123",value为124,在后面的代码中,将会执行e.value = value;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
    //如果当前的p是树结点结构,则交给TreeNode.putTreeVal添加,并返回一个Node结点,这个结点e,就是要添加的结点.
    else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    //否则,说明p是链表结点结构
    else {
        //执行遍历链表操作
        for (int binCount = 0; ; ++binCount) {
            //将e引用p.next结点,遍历到了最后一个结点时,如果仍然没有找到
            if ((e = p.next) == null) {
                //构建一个新的结点给p.next
                p.next = newNode(hash, key, value, null);
                //如果新链表的长度不小于TREEIFY_THRESHOLD(默认为8)
                if (binCount >= TREEIFY_THRESHOLD - 1)
                //将该链表转为树
                    treeifyBin(tab, hash);
                break;
            }
            //如果链表中找到相同的key,(上一个if已经将e引用该结点),结束遍历
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;
        }
    }
    //e!=null说明,之前的链表有相同的key
    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        //在HashMap中这也是一个没有意义的方法,目的是为了给LinkedHashMap去重写
        afterNodeAccess(e);
        return oldValue;
    }
}
//操作加一
++modCount;
//如果size>threshold值,扩容
if (++size > threshold)
    resize();
//在hashMap中是没有任何作用一个方法,可以不用理睬,其目的是为了让其子类LinkedHashMap重写afterNodeInsertion
afterNodeInsertion(evict);
return null;
}

总结putVal过程: 假设要存入的为 k, v,存的位置为hash
1. 先考虑是否需要初始化table
2. table[hash]的位置,是否为null,是null就直接新建一个Node,存入k,v,否则执行第三步
3. 在table中去找有没有和k相等的key,有则找到那个key的Node,后面再将value修改为v,并且return 之前的value; 没有则新建一个Node,执行第四部
具体寻找相等的key过程:

1) 当前的table[hash]可能是TreeNode,也有可能是Node,所以得分两种情况考虑.
TreeNode交给TreeNode的putTreeVal方法去处理
2) Node就通过循环去找,需要注意的是,如果没有找到,新建一个Node存储k,v,需要考虑链表长度是否不少于8,不少于8的话,需要将Node链表结构改为TreeNode树结构

4. size++,modCount++,以及考虑扩容情况的其他操作

思考:
1.不考虑扩容情况,多个线程同时往HashMap里put数据,可能会出现哪些问题?

1). 数据丢失:
如果,A线程的put的key和B线程put的key,两者经过计算后的hash值相等的时候,并且之前A的key和B的key都没有插入过,它们有可能发生,后put的key会覆盖掉先put的key. 如果A,B两个的key值并不相等,那就意味着丢失了一个数据.

2). 数据未修改:
在Node转为TreeNode或者TreeNode转为Node的时候,另一个线程做对该链表或者树的某一个元素进行value的修改时候,将可能会无效.

2.A线程扩容的时候,B线程做put操作,会出现什么问题?
仔细分析源码:
1) 数据丢失
2) 新put进来的数据位置混乱: 原因在,put计算位置的时候用的是旧的table,put进去却放入的是新的扩容后的table.

弄懂了putVal,其他的remove,clear方法就很简单了.


迭代器

 abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

HashMap提供的迭代数据,key,value和Entry

    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

    final class ValueIterator extends HashIterator
        implements Iterator<V> {
        public final V next() { return nextNode().value; }
    }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

从迭代器的源码来看,迭代的时候,获取的是HashMap里的对象

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值