千家信息网

HashMap源码怎么写

发表于:2025-01-31 作者:千家信息网编辑
千家信息网最后更新 2025年01月31日,今天就跟大家聊聊有关HashMap源码怎么写,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。源码学习,边看源码边加注释,边debug,边理解。基
千家信息网最后更新 2025年01月31日HashMap源码怎么写

今天就跟大家聊聊有关HashMap源码怎么写,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

源码学习,边看源码边加注释,边debug,边理解。

基本属性

常量

  • DEFAULT_INITIAL_CAPACITY:默认数组的初始容量 - 必须是2的幂。

  • MAXIMUM_CAPACITY:数组的最大容量

  • DEFAULT_LOAD_FACTOR:哈希表的负载因子0.75

  • TREEIFY_THRESHOLD:在一个桶内由树转换成链表的阈值

  • UNTREEIFY_THRESHOLD:又树转换成链表的阈值

  • MIN_TREEIFY_CAPACITY:在数组长度大于或等于64时才会进行链表转换成树的操作,否则直接扩容

全局变量

  • table:数组对象

  • size:HashMap大小

  • modCount:操作HashMap的总数 for fast-fail

  • threshold: 扩容的阈值

  • loadFactor:哈希表的负载因子,默认是DEFAULT_LOAD_FACTOR值

数据结构

HashMap的数据结构在jdk1.8之前采用的是数组+链表,jdk1.8之后采用了数组+链表/红黑树的结构,如图:

如图所示当链表长度大于8时,链表转换成红黑树。

在jdk1.8中存储数据的节点有两种一种是链表节点Node一种是树节点TreeNode:

Node:

static class Node implements Map.Entry {        final int hash;        final K key;        V value;        Node next;...

TreeNode:

 static final class TreeNode extends LinkedHashMap.Entry {        TreeNode parent;  // red-black tree links        TreeNode left;        TreeNode right;        TreeNode prev;    // needed to unlink next upon deletion        boolean red;...

通过上面的继承关系我们发现TreeNode是继承自Node的。

如果元素小于8个,链表查询成本高,新增成本低 如果元素大于8个,红黑树查询成本低,新增成本高

常见的使用方式

    @Test    public void testHashMap() {        Map map = new HashMap<>();        map.put("1", "1");        map.get("1");        map.size();    }

这是我们使用HashMap最常见的使用方式,下面我就来看下每一步都是怎么实现的。测试代码:

public class HashMapTest {    public static void main(String[] args) {        Map map = new HashMap<>();        for (; ; ) {            map.put(new User(), map.size());            if (map.size() > 1000) {                break;            }        }        map.size();    }    static class User {        @Override        public int hashCode() {            return 1;        }    }}

构造函数

我们使用HashMap第一步是先创建一个HashMap,从上面的语句来看HashMap继承自Map接口,下面我们开看看new HashMap<>()都做了些什么:

    public HashMap() {        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted    }

原来啥都没干,就只是对一个成员变量赋了一个初值。看来数组的初始化和链表的初始化等都是在后面发生的。

put() 方法

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

hash() 方法

    static final int hash(Object key) {        int h;        // 计算key.hashCode()并将更高位的散列扩展(XOR)降低。采用位运算主要是是加快计算速度        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);    }

putVal() 方法

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {        // 数组对象        Node[] tab;        // 通过key值找出对应数组索引位的数据 p = tab[i = (n - 1) & hash]        Node p;        // n 表示数组长度, i表示key值在数组上的索引位        int n, i;        if ((tab = table) == null || (n = tab.length) == 0)            // 判断数组是否为null,如果是则调用resize()方法进行初始化            n = (tab = resize()).length;        if ((p = tab[i = (n - 1) & hash]) == null)            // (n-1)&hash= hash%(n-1),通过该公式找出该值在数组上的索引位。 保证不发生数组越界。            // 如果该索引位为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))))                // 表示key完全相同                e = p;            else if (p instanceof 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                            // 如果链表内的数据已经超过8个则尝试将链表转成红黑树(其实这个时候链表已经有9个节点了,最后一个节点是上一步添加进去的)                            treeifyBin(tab, hash);                        break;                    }                    if (e.hash == hash &&                            ((k = e.key) == key || (key != null && key.equals(k))))                        // key完全相同则走后面的value替换流程                        break;                    p = e;                }            }            if (e != null) { // existing mapping for key                V oldValue = e.value;                if (!onlyIfAbsent || oldValue == null)                    // 在key完全相同的情况下,用新数据去覆盖老数据的value值,并返回老数据的value值                    e.value = value;                afterNodeAccess(e);                return oldValue;            }        }        ++modCount;        // 判断如果hash表的总数大于扩容阈值的时候需要进行扩容        if (++size > threshold)            resize();        afterNodeInsertion(evict);        return null;    }

putTreeVal() 插入红黑树节点

final TreeNode putTreeVal(HashMap map, Node[] tab,                                                                int h, K k, V v) {        Class kc = null;        boolean searched = false;        // 找到根节点        TreeNode root = (parent != null) ? root() : this;        for (TreeNode p = root; ; ) {                // dir 表示两个key的比较结果,ph表示p节点的hash值                int dir, ph;                K pk;                if ((ph = p.hash) > h)                        // 父节点的hash值大于新节点hash值                        dir = -1;                else if (ph < h)                        // 父节点的hash值小于新节点hash值                        dir = 1;                else if ((pk = p.key) == k || (k != null && k.equals(pk)))                        // 表示key完全相同                        return p;                else if ((kc == null &&                                // 判断对key是否实现Comparable接口                                (kc = comparableClassFor(k)) == null) ||                                // 使用Comparable来比较父节点和新节点的key值大小                                (dir = compareComparables(kc, k, pk)) == 0) {                        // 这个查找只会执行一次                        if (!searched) {                                TreeNode q, ch;                                searched = true;                                // 从p的左子树找到对应key的节点                                if (((ch = p.left) != null &&                                                (q = ch.find(h, k, kc)) != null) ||                                                // 从p的右子树找到对应key的节点                                                ((ch = p.right) != null &&                                                                (q = ch.find(h, k, kc)) != null))                                        //表示key完全相同的节点                                        return q;                        }                        // 使用默认比较器比较两个key的大小                        dir = tieBreakOrder(k, pk);                }                TreeNode xp = p;                // 自旋找出新节点的父节点                if ((p = (dir <= 0) ? p.left : p.right) == null) {                        Node xpn = xp.next;                        TreeNode x = map.newTreeNode(h, k, v, xpn);                        // 将新节点放到对应的叶子节点位置                        if (dir <= 0)                                xp.left = x;                        else                                xp.right = x;                        xp.next = x;                        x.parent = x.prev = xp;                        if (xpn != null)                                ((TreeNode) xpn).prev = x;                        // 调整树的平衡                        moveRootToFront(tab, balanceInsertion(root, x));                        return null;                }        }}

treeifyBin() 尝试将链表转换成树的方法

    final void treeifyBin(Node[] tab, int hash) {        int n, index;        Node e;        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)            // 检查数组长度如果小于64则不进行红黑树转换,直接进行扩容            resize();        else if ((e = tab[index = (n - 1) & hash]) != null) {            TreeNode hd = null, tl = null;            do {                // 将key值对应数组索引位上所有链表节点转换成红黑树节点                TreeNode p = replacementTreeNode(e, null);                if (tl == null)                    // 树的根节点                    hd = p;                else {                    p.prev = tl;                    tl.next = p;                }                tl = p;            } while ((e = e.next) != null);            if ((tab[index] = hd) != null)                // 由链表转换成了红黑树                hd.treeify(tab);        }    }

treeify() 将链表转换成红黑树

final void treeify(Node[] tab) {        TreeNode root = null;        // for循环遍历链表所有节点        for (TreeNode x = this, next; x != null; x = next) {                // 给下一个节点赋值                next = (TreeNode) x.next;                x.left = x.right = null;                // 给root节点赋值                if (root == null) {                        x.parent = null;                        x.red = false;                        root = x;                } else {                        K k = x.key;                        int h = x.hash;                        Class kc = null;                        for (TreeNode p = root; ; ) {                                int dir, ph;                                K pk = p.key;                                // 比较root节点和X节点的hash值大小                                if ((ph = p.hash) > h)                                        dir = -1;                                else if (ph < h)                                        dir = 1;                                else if ((kc == null &&                                                (kc = comparableClassFor(k)) == null) ||                                                (dir = compareComparables(kc, k, pk)) == 0)                                        dir = tieBreakOrder(k, pk);                                // 将X节点添加到红黑树                                TreeNode 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);}

resize() 扩容方法

    final Node[] resize() {        // 扩容前的数组        Node[] oldTab = table;        // 获取数组长度        int oldCap = (oldTab == null) ? 0 : oldTab.length;        // 扩容阈值        int oldThr = threshold;        // 扩容后的数组长度和扩容阈值        int newCap, newThr = 0;        if (oldCap > 0) {            if (oldCap >= MAXIMUM_CAPACITY) {                // 如果数组容量已经大于最大容量(1<<30)了那么将不在进行扩容                threshold = Integer.MAX_VALUE;                return oldTab;            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                    oldCap >= DEFAULT_INITIAL_CAPACITY)                // 1. 数组长度直接扩容2倍                // 2. 扩容阈值也扩容2倍                newThr = oldThr << 1; // double threshold        } else if (oldThr > 0) // initial capacity was placed in threshold            newCap = oldThr;        else {               // zero initial threshold signifies using defaults            // 值为0表示还没有初始化,然后给数组初始大小和扩容阈值赋值            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;        @SuppressWarnings({"rawtypes", "unchecked"})        // 根据newCap值创建数组        Node[] newTab = (Node[]) new Node[newCap];        table = newTab;        // oldTab != null表示是扩容,否则表示是初始化        if (oldTab != null) {            for (int j = 0; j < oldCap; ++j) {                Node e;                if ((e = oldTab[j]) != null) {                    // 将老数组的对应索引位置为NULL,方便GC回收                    oldTab[j] = 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                        // 和jdk1.7不一样,这里扩容后链表顺序不会发生改变                        // 低位桶的头节点                        Node loHead = null, loTail = null;                        // 高位节点高位桶的头节点                        Node hiHead = null, hiTail = null;                        Node next;                        do {                            // 保存下一个节点,待下次循环使用                            next = e.next;                            // e.hash & oldCap算法可以算出新的节点该分配到那个索引位,                            // 这也是为什么数组长度一定要是2的n次幂,否则该算法不可用                            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);                        // 如果j=0 并且oldCap=16,那么低位桶就是0的索引位,高位桶就是0+16的索引位                        if (loTail != null) {                            loTail.next = null;                            // 设置低索引位的头结点                            newTab[j] = loHead;                        }                        if (hiTail != null) {                            hiTail.next = null;                            // 设置高索引位的头结点                            newTab[j + oldCap] = hiHead;                        }                    }                }            }        }        return newTab;    }

从put方法的源码我们发现:

  • HashMap数组的初始化时在put元素的时候发生的

  • 发生扩容条件有两个:一个是一个桶内链表数据大于8并且数组长度小于64;HashMap总size大于扩容的阈值。任意满足一个都会发生扩容。

  • 在扩容的时候,jdk1.8链表的顺序将不会再发生变化,从而解决了1.8以前链表扩容引发的死循环问题。HashMap中是如何形成环形链表可参考这个

  • 数组长度始终是2的n次幂。这样做使我们可以直接使用位运算来计算key的索引位;在扩容的时候可以直接使用位运算来计算高低位索引的节点。

  • 链表转换成树的条件是只有当一个桶的元素超过8个并且数组长度大于等于64。

  • 链表转换成树只发生在一个桶内,也就是说在HashMap的数据结构中可以一些桶是链表,一些桶是红黑树。

红黑树部分可以参考

  • https://www.jianshu.com/p/7993731be0cf

get() 方法

    public V get(Object key) {        Node e;        return (e = getNode(hash(key), key)) == null ? null : e.value;    }

getNode() 方法

    final Node getNode(int hash, Object key) {        Node[] tab;        Node first, e;        int n;        K k;        // 先根据hash值找到数据所在桶内的根节点(头结点)        if ((tab = table) != null && (n = tab.length) > 0 &&                (first = tab[(n - 1) & hash]) != null) {            // 如果根节点(头结点)就是我们要找的直接的返回            if (first.hash == hash && // always check first node                    ((k = first.key) == key || (key != null && key.equals(k))))                return first;            if ((e = first.next) != null) {                // 树结构的情况                if (first instanceof TreeNode)                    return ((TreeNode) first).getTreeNode(hash, key);                // 链表结构情况                do {                    // 自旋查找                    if (e.hash == hash &&                            ((k = e.key) == key || (key != null && key.equals(k))))                        return e;                } while ((e = e.next) != null);            }        }        return null;    }

tableSizeFor() 方法

我们在初始化HashMap的时候,我们传入的初始化大小可能不是2的n次幂。这时我们需要调用tableSizeFor方法找出和cap最接近的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;    }

HashMap非线程安全的表现

HashMap是一个线程不安全的类,主要表现在:

  1. 在并发操作下size的统计会出错

  2. 并发操作下添加节点有可能会丢失数据

  3. 在并发操作下JDK1.7及以前在扩容的时候链表有可能会形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。

HashMap空间效率

HashMap其实是一种空间利用率很低的数据结构,以HashMap结构为例,我们具体分析一下HashMap空间效率。

有效数据空间

在HashMap结构中,只有Key和Value所存放的两个长整型数据是有效数据,共16字节(2×8字节)。

实际占用空间

Key和Value这两个长整型数据包装成java.lang.Long对象之后,就分别具有8字节的Mark Word、8字节的Klass指针,再加8字节存储数据的long值。然后这2个Long对象组成Map.Entry之后,又多了16字节的对象头,然后一个8字节的next字段和4字节的int型的hash字段,为了对齐,还必须添加4字节的空白填充,最后还有HashMap中对这个Entry的8字节的引用,这样增加两个长整型数字,实际耗费的内存为(Long(24byte)×2)+Entry(32byte)+HashMap Ref(8byte)=88byte。

空间利用率

空间效率为有效数据空间除以实际占用空间,即16字节/88字节=18%,这确实太低了。

源码

https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-concurrent 工程

layering-cache

为监控而生的多级缓存框架 layering-cache这是我开源的一个多级缓存框架的实现,如果有兴趣可以看一下

看完上述内容,你们对HashMap源码怎么写有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注行业资讯频道,感谢大家的支持。

节点 数组 数据 索引 字节 方法 结构 长度 阈值 空间 时候 源码 两个 大小 数据结构 结点 相同 对象 高位 位置 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 网络安全与前端开发那个稳定 上海网络安全台账 网络安全保卫大队召开网络 网络安全实验教材 网络安全防范措施要求哪几点 书店数据库设计 网络安全征文的评语 郑州支付宝网络技术有限公司 泰拉瑞亚服务器房子怎么设定 网络安全手抄报简单素颜 数据库sql创建登录名 服务器cpu测试软件 互联网科技竞赛 一网推江苏网络技术有限公司 计算机网络技术和铁路有关吗 gpu 游戏服务器 服务器做重大变更前一般注意什么 天津智慧城管软件开发系统 重建数据库控制文件 计算机软件开发工程师考试科目 石油化工概算应用数据库2017 dhcp服务器配置实验 过计算机一级的软件开发 黄石软件开发制作平台 强化网络安全 预防网络沉迷征文 传奇怀旧雄狮服务器 怎么添加一行数据库 工控网络安全设备国产化 浦东新区智能软件开发成本 南京新网互联网络科技有限公司
0