java源码---hashmap源码分析(jdk1.8)

 

一、新的琢磨和旧的理解

之前大致写了一篇hashmap的源码分析,地址。

但总觉得理解有很多错误的理解,比如之前只理解数据存储在hashmap中开始是数组,后来是链表,再后来是红黑二叉树,但最近几周感觉理解有问题,重新理解了下,才觉得大错特错。

其实真实的结构却是这样的

java源码---hashmap源码分析(jdk1.8)_第1张图片

二、hashmap新的源码解析

1、创建hashmap对象

HashMap map = new HashMap<>();
Map map2 = new HashMap();
Map map3 = new HashMap(15);

 源码的实现(部分源码)

        /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    transient Node[] table;

    transient int modCount;
    final float loadFactor;
    int threshold;
    
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

可以发现,当不指定大小创建HashMap集合时,他会默认指定一个大小为16的数组结构。当指定大小呢?

指定大小创建HashMap集合对象

    public HashMap(int initialCapacity) { //指定大小创建hashmap集合对象
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    //上面this调用的方式
    public HashMap(int initialCapacity, float loadFactor) {
        //校验给定的容量大小的合法性
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        //当给定的容量大小有效时,重新计算合适的大小
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    //如果给定的容量大小不是2的n次幂,则让他成为2^n
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

2、向hashmap集合中增加数据(重要)

map.put(1, "香蕉");
map.put(1,"bunana");

首先别奇怪我添加了重复的key,下面会有说到,我们先看源码的实现,源码很长,我们分块分析

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        //判断hashmap是否存在
        if ((tab = table) == null || (n = tab.length) == 0)
            //如果不存在,则采用resize()构建新的node[],并赋值给tab
            n = (tab = resize()).length;

        //n=tab.length,所以此处(n-1)&hash是计算数据在数组中保存的位置下标
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果数组下标内无数据,则直接将数据保存至数组中
            tab[i] = newNode(hash, key, value, null);
        else {
            //计算到这个数据保存到数组下标的位置存在别的数据时
            Node e; K k;
            //判断key是否一致(数组类型数据)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

            //判断当前数组这个下标内的数据类型是否为二叉树
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            
            //如果数组中指定下标中保存的数据类型是链表
            else {
                //遍历链表  获取各项节点信息
                //注意一点:jdk1.7是链表头追加,1.8才是尾追加
                for (int binCount = 0; ; ++binCount) {
                    //2、如果最后一个数据的next节点为null  则保存数据
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //3、保存数据后,判断保存后的链表的长度,度过大于等于8-1时,则将链表转化为二叉树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //1、判断每个节点中的key是否相同,相同则做值的覆盖操作
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            
            //这里是为了在put操作存放相同key时,将旧的key的value返回出去
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }

        //每次put都增加数组中保存的数据长度,如果大于 容量*0.75f 则进行扩容操作
        ++modCount;
        if (++size > threshold)
            //执行扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在最开始的put方式中,他的hash做了什么?

    static final int hash(Object key) {
        int h;
        //将数据的key信息进行hashcode运算,并将高16位和低16位做异或运算,求取hash数据
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    //这样操作的hash数据有什么好处?
    1、下面的添加代码有分析,数据是如何保存至数组的指定位置的
    2、如果不进行高、低16位的异或运算,则计算的数组下标位会有较高的几率出现一样
    3、当数组中的数据填充一样时,他会使用链表或者红黑二叉树进行数据的保存操作

 

我们针对putVal的操作,分3次分析:

java源码---hashmap源码分析(jdk1.8)_第2张图片

 1)的分析:

当我们在创建一个新的HashMap时,作为类的成员属性,他会初始化一个table

transient Node[] table;

1中的操作,将hashmap类创建时的table属性赋值给局部变量 Node[] tab; 如果 table属性为null 或者 table的大小为0,表示这个table还是初始的,所以需要做的操作则是创建数组了,所以采取了resize();

final Node[] resize() {
        //将全局成员变量赋值给局部变量
        Node[] oldTab = table;
        //如果全局的成员变量是null,则大小给定为0,否则大小则是它本身的大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //将成员的容量和容压系数乘积保存至局部变量
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果这个容器的大小不为0(初始化创建的hashmap对象不会进行此项)
        if (oldCap > 0) {
            //如果容器大小超过最大上限,则采取最大上限作为容器容量
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果此时的容器扩容一倍后依旧小于最大容量 并且 高于初始的大小
               //前面说到容量为2^n
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //将最大容压容量进行扩容一倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果是初始的,就初始化各项参数信息
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;

        //创建新的node数组对象
        @SuppressWarnings({"rawtypes","unchecked"})
            Node[] newTab = (Node[])new Node[newCap];
        table = newTab;
        
        //-----------------------------------非初始化start
        //初始不会进入此项判断
        //如果大小将要超过最大容压大小(总大小和容压系数的乘积)
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node e;
                //原数组中的数据保存采取hash与运算容量大小,所以可能存在数组下标为空的数据,此处是为了过滤空数据
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //如果链表中的next参数为null
                    if (e.next == null)
                        //计算新的数组中,数据所在的下标,并存入数据(数组)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //二叉树类型交给二叉树的方式处理
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //链表数据的处理
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;
                        do {
                            next = e.next;
                            //如果链表中数据的hash成员数据值 与 容器大小为0
                            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;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        //----------------------------------------非初始化end
        
        //1、如果是初始化操作,则创建新的Node[] 对象后,将其直接返回出去
        //2、如果是扩容的node[] ,则需要创建新的大小的容器,并将旧数组中的数据移入新的数组中并返回出去
        return newTab;
    }

resize方法也很长,我们只需要关注你是新创建的hashmap还是添加数据后,容量要上限了,进行的扩容操作。

2)的分析

n = tab.length,因为数组的下标是从0开始的,所以n-1表示数据在数组中的下标范围!

如果数组的容量和node中hash成员数据的与运算数据为null时(计算的下标,在Node[] table中不存在数据),则将计算出保存数据的位置值赋值给i,同时使用

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

创建新的newNode对象保存新的数据,并存在Node[]中。(上面就说到了,创建hashmap几个时,旧创建了Node[] table;)

3)的分析

java源码---hashmap源码分析(jdk1.8)_第3张图片

当hashmap对象存在,且采取key计算的hashcode的高、低16位计算的hash数据,与容器容量与运算后,对应的下标中,数组中有对应的数据

例如:

java源码---hashmap源码分析(jdk1.8)_第4张图片

他可能会有以下几项操作:

  • key相同,所以计算到的下标存在,则覆盖旧数据
  • key不同,但运算处的下标存在,则判断当前的数据类型是链表还是红黑树

所以我们继续拆分

3.1)如果数据key一样,则进行value的替换操作

3.2)如果数据是二叉树,则采取二叉树进行数据的分析 

3.3)如果数据是链表

else {
                //遍历链表
                for (int binCount = 0; ; ++binCount) {
                    //如果发现指定数组中保存的Node数据next数据为null,则将新的数据保存至next属性下
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果次数的链表长度大于等于定义的上限参数(8-1),则将链表转化为红黑二叉树的方式,存入数组的指定下标区内
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //链表转换红黑二叉树
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果遍历发现node数组中的指定下标中存入的链表数据的hash值一样,切两者key信息一样,则进行值的覆盖操作
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

//此项代码也不能忽视,因为当存在重复的key信息时,他是进行了值的覆盖,并将旧的数据返回出去
if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

 3.4)在putVal操作要完成之前,都判断次数的大小是否超过了最大容压大小,如果大于则重新定义数组的大小

++modCount;
        if (++size > threshold)
            resize();

3、最后做点总结和补充

3.1、我的二叉树学的不好,最近还在研究中,就没具体说明二叉树保存数据是如何实现了。

3.2、hashmap保存数据是最初的数组,当计算的数组下标存在数据后,会判断此时的数据是链表结构还是二叉树结构,在根据指定的结构分析每个元素的hash属性值和新加入的数据hash属性值是否一致,一致则采取equals方式对比内容是否一样,如哦一样则覆盖,不一样则找到链表最末尾的next,将值拼接上去。

3.3、最后说下数组中保存数据,下标的计算

首先,将数据的key采取object类的hashcode()计算真实的hashcode值,

其次,将计算后的hashcode值高16位和低16位进行 异或  算法,这样可以更好的保证低位的数据的随机性,

最后,将计算的hash属性值和容量减一的二进制数据进行与运算,因为是与容量减一进行与运算,所以最大的下标为容量大小减一!

3.4、二进制的集中运算方式

java源码---hashmap源码分析(jdk1.8)_第5张图片

总结:写的有些杂乱,后期再慢慢整理优化吧,看明白了那个复杂的putVal,其实感觉还是蛮简单的。要有耐心吧。 

2019.07.27 在网上找到个比我的写的清晰的博客:地址。

你可能感兴趣的:(java基础,hashmap)