jdk1.8 HshMap.put()方法详解

简单介绍一下JDK1.8 HashMap的数据如下
这里写图片描述

HashMap存放的是一个Node

1、 jdk1.8 HshMap.put()方法详解

由上图可见,hashMap数据结构是:数组链表,
数组里面存放的是链表的第一个节点Node,这个节点是个单向链表结果,有一个next()指向下一个节点

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

2、hash(key)源码如下:

设计者想了一个顾全大局的方法(综合考虑了速度、作用、质量),就是把高16bit和低16bit异或了一下,返回一个掩码。设计者还解释到因为现在大多数的hashCode的分布已经很不错了,就算是发生了碰撞也用O(logn)的tree去做了。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞

// 计算hash值,hashcode值是32位,讲高16位和第16位做异或操作,返回一个掩码
// 一个简单的操作,不会影响效率
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 计算桶的下标(桶的长度-1与hash值)
// 因为桶的长度n不会很长(默认长度DEFAULT_INITIAL_CAPACITY为16)
// 因此:key的高位和地位都参与了桶的下标计算,减少了碰撞
(n - 1) & hash

3、putVal()

put方法主要做了以下操作

1、判断数组(哈希桶)是否为空,如果为空,重新计算一下大小(初始化一个桶)
2、获取要插入元素在 哈希桶中的位置(tab[(n - 1) & hash])
3、如果桶的这个位置没有数据,new一个节点,存放桶里面
4、如果有数据,判断这个位置的第一个节点是否相等(相等直接替换赋值,返回old值)
5、如果和第一个节点不相等,再判断第一个节点的下一个节点是否是红黑树,如果是,通过红黑树方式put
6、如果和第一个节点不相等,也不是红黑树,那么就按照链表结构处理。判断是否和链表的下一个相等,如果相等,直接复制替换。
7、如果链表里面没有,如果大于了阙值(大于等于TREEIFY_THRESHOLD,默认为8), 需要扩容的大小,链表转换成红黑树;

具体源码如下:

// onlyIfAbsent在true的情况下不改变已有value值,evict(驱逐)在false的情况下table为创作模式.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {

    Node<K, V>[] tab;
    Node<K, V> p;
    int n, i;

    // 1.判断数组(桶)是否为空,如果为空,重新计算一下大小(初始化一个桶)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    /**
     * 2. 获取要插入元素在 哈希桶中的位置,同时将值赋值给p(定位了桶的位置)
     *   i = (n - 1) & hash:就是Node在哈希桶中tab的下标(位置)
     */
    //  如果这个位置没有Node,
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 直接创建一个新的Node
        tab[i] = newNode(hash, key, value, null);

        // 如果有这个位置的node不为null,原来这个桶的位置上有Node
    else {
        Node<K, V> e;
        K k;

        // 如果第一个位置和put的hash一致,直接替换
        // 桶里面存放的Node,node是一个单向链表结构
        if (p.hash == hash
                && ((k = p.key) == key
                || (key != null && ey.equals(k))))
            e = p;// 讲值赋值给e(其实后面是做替换)

            // 如果p链表的类型是属于红黑树,用红黑树的方式进行put
        else if (p instanceof TreeNode)
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);

            // 如果不是红黑树,就是链表,按链表方式处理
            // 定位到这个hash桶了 但是这里面是链表(没有进行过树化)
        else {

            // 遍历链表
            for (int binCount = 0; ; ++binCount) {

                // p节点的next为空 直接在后面插入,新建一个节点
                // e是p节点的下一个节点
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);

                    // 链表转变为红黑树。如果本来是链表 而且长度超过了8 那么就进行树化 
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }

                // 如果和下一个节点相等,且不为null,表明找到当前节点,结束循环
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;

                // 讲e(p.next())赋值给p,继续下一次循环
                p = e;
            }
        }

        // 如果找到了节点,说明关键字相同,进行覆盖操作,直接返回旧的关键字的值
        // 注意e是p的下一个节点
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); //空实现,里面没有实现方法
            return oldValue;
        }
    }

    // 没有找到相应的key执行下面操作
    // 修改次数+1 和fastRemove()有关也和并发修改有关
    ++modCount;
    // 容量hashmap存放node的个数+1,如果大于了阙值 需要扩容的大小
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); //空实现,里面没有实现方法
    return null;
}

4、put方法里面用到的函数

1、resize() 重新计算容量,或者是扩容方法
2、p.putTreeVal(this, tab, hash, key, value) 存放数据倒红黑树里面
3、treeifyBin(tab, hash) 将链表转换为红黑树

[4.1] resize()重新计算容量

源码:

final HashMap.Node<K, V>[] resize() {
    // oldTable:当前的数组(旧的hash桶)
    HashMap.Node<K, V>[] oldTab = table;
    // 如果你是新创建的话 表的大小就是0 否则就是原来的大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 第一次是为0的 代表DEFAULT_INITIAL_CAPACITY = 1 << 4;
    int oldThr = threshold;
    // 新的容量,新的阙值:扩容后存放数据的大小(及hashmap的size)
    int newCap, newThr = 0;
    //如果旧的容量大于0
    if (oldCap > 0) {
        // 如果旧的容量大于等于最大容量,返回旧
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 扩容大小 = 最大范围
            threshold = Integer.MAX_VALUE;
            // 直接返回就旧的数组
            return oldTab;
        } 

        // 将旧的数组(桶)扩容一倍赋值给newCap(数组)
        // 如果newCap小于最大容量,并且旧容量>=初始化16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY 
                && oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 存放数据的size扩大一倍。左移1位,新size容量 = 旧size容量*2
            newThr = oldThr << 1; 
    }

    // 如果旧的存放数据的容量>0,那么新的桶大小=旧的容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 说明是 threshold为0的时候的情况
    else { // zero initial threshold signifies using defaults
        // 新的容量为默认容器的容量
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 新的阙值为 默认的容量 * 负载因子
        newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果新的如果新的扩容为0 ,上面走的else if (oldThr > 0)
    // 计算新的阙值
    if (newThr == 0) {
        // 计算得到新的阙值
        float ft = (float) newCap * loadFactor;
        // 如果新的桶容量小于最大容量 并且 阙值小于最大容量,新的阙值=ft,否则新的阙值= int型最大值
        newThr = (newCap < MAXIMUM_CAPACITY 
                && ft < (float) MAXIMUM_CAPACITY ?
                (int) ft : Integer.MAX_VALUE);
    }
    // 阙值 = 新的阙值
    threshold = newThr;
    @SuppressWarnings({"rawtypes", "unchecked"})

    // 建一个新的哈希数组桶 大小为新的容量
    HashMap.Node<K, V>[] newTab = (HashMap.Node<K, V>[]) new HashMap.Node[newCap];
    table = newTab;
    // 如果旧的数组(桶),遍历数组
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            HashMap.Node<K, V> e;
            // 如果旧的hash桶的元素不为null  e为旧的hash桶的元素
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;

                // 如果hashmap就只有一个元素
                if (e.next == null)
                    // 那么在新的hash桶给你安排一个位置
                    // 位置是你的hash值&新的桶的容量(size)-1,
                    // 这里是hashmap存放数据的一种算法,
                    newTab[e.hash & (newCap - 1)] = e;

                // 如果不只一个元素并且是红黑树
                else if (e instanceof HashMap.TreeNode)
                    // 分割  将树中的节点 分割到高位或者地位上去  
                    ((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                // 其他情况,就是一个普通的链表
                else { // preserve order
                    // 如果扩容后,元素的index依然与原来一样,那么使用这个低位head和tail指针
                    HashMap.Node<K, V> loHead = null, loTail = null;
                    // 如果扩容后,元素的index=index+oldCap,那么使用这个高位head和tail指针
                    HashMap.Node<K, V> hiHead = null, hiTail = null;
                    // 下一个节点
                    HashMap.Node<K, V> next;
                    do {
                        next = e.next;
                        // 这个地方直接通过hash值与oldCap进行与操作得出元素在新数组的index
                        // 看是否需要进行位置变化 新增位的值 不需要变化就放在原来的位置
                        // 这里的判断需要引出一些东西:oldCap 假如是16,那么二进制为 10000,扩容变成 100000,也就是32.
                        // 当旧的hash值 与运算 10000,结果是0的话,那么hash值的右起第五位肯定也是0,那么该于元素的下标位置也就不变。
                        if ((e.hash & oldCap) == 0) {
                            // 第一次进来时给链头赋值
                            if (loTail == null)
                                loHead = e;
                            // 给链尾赋值
                            else
                                loTail.next = e;
                            // 重置该变量
                            loTail = e;
                        }
                        // 需要变化 就构建高位放置的链表
                        // 如果不是0,那么就是1,也就是说,如果原始容量是16,那么该元素新的下标就是:原下标 + 16(10000b)
                        else {
                            // 第一次进来时给链头赋值
                            if (hiTail == null)
                                hiHead = e;
                            // 给链尾赋值
                            else
                                hiTail.next = e;
                            // 重置该变量
                            hiTail = e;
                        }
                    } while ((e = next) != null);

                    // 理想情况下,可将原有的链表拆成2组,提高查询性能。
                    if (loTail != null) {
                        // 销毁实例,等待GC回收
                        loTail.next = null;
                        // 置入数组(桶bucket)中
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 在新链表的位置赋值
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

[4.2] p.putTreeVal(this, tab, hash, key, value) 存放数据倒红黑树里面

下一节详细讲解红黑树
[4.3] treeifyBin(tab, hash)讲红黑树转换为链表

树化过程:
1、根据哈希表中元素个数确定是扩容还是树形化
2、树化
遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容
但是我们发现,之前的操作并没有设置红黑树的颜色值,现在得到的只能算是个二叉树。在 最后调用树形节点 hd.treeify(tab) 方法进行塑造红黑树

/**
 * Replaces all linked nodes in bin at index for given hash unless
 * table is too small, in which case resizes instead.
 */
// 大体意思:在给定的散列中,替换桶中的所有链接节点,除非表太小,在这种情况下,可以进行调整。
// 及树化
final void treeifyBin(HashMap.Node<K, V>[] tab, int hash) {
    int n, index;
    HashMap.Node<K, V> e;
    // 如果当前哈希表为空,或者哈希表中元素的个数小于 进行树形化的阈值(默认为 64),就去新建/扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // hash表的元素个数大于MIN_TREEIFY_CAPACITY,树华操作
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 红黑树的头、尾节点
        HashMap.TreeNode<K, V> hd = null, tl = null;
        do {
            // 新建一个树节点,存放数据为当前链表节点e一直
            HashMap.TreeNode<K, V> p = replacementTreeNode(e, null);

            // 设置头节点
            if (tl == null)
                hd = p;
            // 第二次循环后,设置上一个节点为tl,当链表节点e设置为tl的下一个节点
            else {
                p.prev = tl;
                tl.next = p;
            }
            // 赋值,进行下一次循环
            tl = p;
        } while ((e = e.next) != null);

        // 将树的头节点放到hash表中,然后给树标记颜色,将二叉树转变为红黑树
        if ((tab[index] = hd) != null)
            // 二叉树转变为红黑树
            hd.treeify(tab);
    }
}


// 树化
final void treeify(HashMap.Node<K, V>[] tab) {
    HashMap.TreeNode<K, V> root = null;
    for (HashMap.TreeNode<K, V> x = this, next; x != null; x = next) {
        next = (HashMap.TreeNode<K, V>) x.next;
        x.left = x.right = null;

        // 确定头结点,为黑色
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        // 确定根节点后,设置子节点
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (HashMap.TreeNode<K, V> p = root; ; ) {

                // ph存放根节点的hash(父节点的hash),
                // dir是存放 比较当前节点和父节点的大小的一个变量
                int dir, ph;
                K pk = p.key;

                // 如果父节点hash大于当前节点的hash,dir=-1
                if ((ph = p.hash) > h)
                    dir = -1;

                    // 如果父节点hash小于当前节点的hash,dir=1
                else if (ph < h)
                    dir = 1;

                    // kc = comparableClassFor(k)) == null):kc的类型==null
                    // dir = compareComparables(kc, k, pk)) == 0:比较kc的类型,当前key,父节点key是否相等
                else if ((kc == null &&
                        (kc = comparableClassFor(k)) == null) ||
                        (dir = compareComparables(kc, k, pk)) == 0)
                    // 比较k和pk的内存地址(0,-1,1)
                    dir = tieBreakOrder(k, pk);

                // 如果左右节点为空的时候,设置子节点,结束循环
                HashMap.TreeNode<K, V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}
### 回答1: JDK 1.8 tar.gz是指Java Development Kit(JDK)的1.8版本的压缩文件格式为tar.gz。 JDK是Java开发工具包的缩写,它是由Oracle公司提供的用于开发和运行Java应用程序的软件包。JDK 1.8是Java的第8个主要版本,在其发布时具有重要的新特性和改进。 tar.gz是一种常见的压缩文件格式,在Linux和Unix系统中广泛使用。它是通过将文件和目录打包成一个tar文件,然后使用gzip进行压缩而创建的。tar.gz文件既可以用于文件的备份,也可以用于文件的传输和分发。 JDK 1.8 tar.gz文件通常用于安装JDK 1.8版本。要使用JDK 1.8,首先需要下载对应的tar.gz文件,然后解压缩该文件。解压缩后,会得到一个包含JDK 1.8所有必要文件的目录结构。 安装JDK 1.8需要根据操作系统的不同执行不同的步骤。在Linux和Unix系统上,可以通过设置环境变量来配置JDK 1.8的路径,以便可以在终端中直接使用javac和java命令。 JDK 1.8提供了许多新特性,包括Lambda表达式、方法引用、函数式接口、默认方法等。这些功能使得Java编程更加简洁高效。因此,JDK 1.8 tar.gz文件的下载和安装对于Java开发人员来说非常重要。 ### 回答2: JDK 1.8 tar.gz 是指 Java Development Kit 1.8 的压缩文件格式。JDK 是 Java 开发工具包的缩写,是用于开发和编译 Java 程序的软件包。1.8 表示 JDK 的版本号,即 JDK 1.8 代表 Java 开发工具包的第八个主要版本。tar.gz 是常见的文件压缩格式,其中 tar 表示归档(Archive),gz 表示使用 Gzip 压缩算法进行压缩。 JDK 1.8 tar.gz 文件通常包含了 JDK 1.8 的所有组件和库,可以在 Linux 或类似操作系统上使用。通过解压这个压缩文件,可以获得 JDK 1.8 的文件目录结构,包括 Java 编译器、运行时环境、开发工具和相关库等。这些文件可以帮助开发者在其计算机上进行 Java 编程和应用的开发、测试和部署。 要使用 JDK 1.8 tar.gz 文件,首先需要将其解压缩到目标文件夹。可以使用解压缩工具如 tar 命令来进行解压缩。解压后,可以设置环境变量,指向解压后的 JDK 1.8 文件夹,这样就可以在命令行或 IDE 中使用 JDK 1.8 的命令和工具。 JDK 1.8 引入了许多新的特性和改进,例如 Lambda 表达式、函数式接口、Stream API、新的时间日期 API 等等。这些新特性可以提高开发效率、简化代码编写,并且使得 Java 在处理并发、函数式编程等方面变得更强大和灵活。 总之,JDK 1.8 tar.gz 是 Java Development Kit 1.8 的压缩文件格式,包含了 Java 开发工具包的各个组件和库,可以帮助开发者进行 Java 开发和应用的编译、测试和部署。 ### 回答3: JDK是Java Development Kit的缩写,是用于Java程序开发的软件开发工具包。JDK1.8tar.gz是指JDK1.8版本的压缩文件。 tar.gz是一种常见的文件压缩格式,常用于Linux和Unix系统。tar是指tarball,是一种对多个文件进行打包的工具。gz是指gzip,是一种数据压缩程序。tar.gz就是将多个文件打包成tar格式后再使用gzip进行压缩得到的文件。 JDK1.8tar.gz文件可以通过解压缩工具进行解压缩,得到包含JDK1.8版本所有文件和目录的文件夹。这些文件和目录包括JDK的运行环境、编译器、调试工具等,可以用于开发和运行Java程序。 JDK1.8版本是Java的其中一个重要版本,它提供了许多新的特性和改进,包括Lambda表达式、函数式编程、Stream API等。使用JDK1.8版本可以更加方便地开发和管理Java程序,提高程序的性能和效率。 总之,JDK1.8tar.gz是用于Java程序开发的JDK1.8版本的压缩文件,可以通过解压缩得到JDK1.8的所有文件和目录,用于开发和运行Java程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值