细说java系列之HashMap原理

类图

在正式分析HashMap实现原理之前,先来看看其类图。

源码解读

下面集合HashMap的put(K key, V value)方法探究其实现原理。

// 在HashMap内部用于存放插入数据的是一个名为"table"的一维Node对象数组
// Node对象为实际存放插入数据Key和Value的数据结构
transient Node<K,V>[] table;

// 外部调用插入数据的接口方法
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<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        // 第一次插入数据时,初始化table数组
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 变量n为HashMap当前容量大小,实际上就是table数组的容量大小
        // 将(n-1)与插入数据Key的hashcode值进行逻辑与运算,找到一个随机位置i
        // 如果table[i]值为null,说明该位置还没有存放数据,新建一个Node对象并存放在table[i],本次插入完毕,返回null值
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果table[i]值不为null,说明该位置已经存放了数据,继续寻找插入数据的位置
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 如果新插入数据Key的hashcode值与table[i]位置存放对象Key的hascode值相同
            // 并且新插入数据Key与table[i]位置存放对象的Key引用的是同一个对象或者它们相等(通过equals方法比较)
            // 则使用新插入数据的Value替换table[i]位置存放对象的Value,本次插入完毕,返回之前存放在该位置对象的Value值
            e = p;
        else if (p instanceof TreeNode)
            // 如果table[i]位置存放对象属于TreeNode类型,进行特别处理
            // 为什么需要判断是否为TreeNode类型?
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 如果新插入数据Key不与table[i]位置存放对象Key相同,那么寻找一个满足如下条件的位置,将新数据插入到对应位置
            // 条件1:如果table[i]位置对象的next属性为null,直接通过该next属性引用插入数据新建的Node对象,并返回null
            // 条件2:如果table[i]位置对象的next属性不为null,那么就在该位置对象链表上寻找一个插入新数据的位置,在这个过程中根据如下满足条件进行处理
            // 条件3:如果插入数据的Key与链表上的某个Node对象的Key相同,那么使用新插入的Value替换该Node对象的Value,并返回该Node之前的Value值
            // 如果不满足上诉3个条件,将插入数据保存在table[i]位置对象链表的末端,并返回null
            // 总结:HashMap存放实际数据的是一个一维数组,而每一个数组元素又支持链表结构
            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;
}

将上述HashMap实现插入数据的过程以插入4个数据为示例描述如下:
1.插入第一个数据时,初始化HashMap内部名为“table”的一维数组,默认大小为16,每一个数组元素值为null。

寻找一个插入数据的位置i,这在HashMap中的实现非常巧妙,这个插入位置通过如下表达式计算得到:i = (n - 1) & hash。其中,n为当前HashMap的容量,其实就是内部table数组的大小,hash为插入数据Key的hashCode值。通过该表达式将会随机找个一个插入位置i,i的值范围为[0,n-1]。必须注意的是: 插入位置是随机的!并不是按照一维数组的顺序插入方式,这是因为HashMap这个数据结构的特点所决定的。因为是插入第一个数据,所以随机找到的位置“i=3”处对象为null值,因此直接在该位置处插入一个Node对象。本次插入操作完毕,返回null值。

2.插入第二个数据时,先随机找到一个插入位置“i=1”,而且该位置处的对象为null值,说明还没有存放任何数据,直接在该位置处插入一个Node对象。本次插入操作完毕,返回null值。

3.插入第三个数据时,随机找到插入位置“i=1”,该位置上已经存放了数据;并且插入数据的Key不与该位置Node对象的Key相同(Key相同的条件时:首先必须hashCode值相同,并且他们引用的是同一个对象或者他们通过equals()方法比较时相等),此时需要将新插入数据保存到该位置Node对象的next属性中(看起来像是链接到该位置Node对象的尾部)。本次插入操作完毕,返回null值。

4.插入第四个数据时,随机找到插入位置“i=1”,该位置上已经存放了数据;并且插入数据的Key与该位置Node对象的Key相同,此时使用新插入数据的Value替换该位置Node对象当前的Value值。本次插入操作完毕,返回该位置Node对象之前的Value值。

上述示例描述的就是HashMap插入数据的原理,实际上除了上述描述的核心操作之外,在返回值之前需要判断HashMap当前的容量是否能够存储更多插入的数据,根据判断之后可能会进行扩容,如下代码所示:

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

总结

1.先明确一个事实,HashMap内部实际存放数据的是一个一维数组,但是存储的元素类型支持链表结构。所以,存放数据之后的HashMap看起来像是一个“二维数组”(注意: 并不是真正的二维数组)。

2.判断HashMap存放对象Key是否相同,方法如下:

  • 新插入Key的hashCode值必须与已经存在对象Key的hashCode值相等,这是前提
  • 新插入Key与已存在对象Key引用的是同一个对象,或者他们通过equals()方法比较时相等

3.HashMap内部名为“table”的一维数组可能存在“存不满”数据的情况,因为插入数据的位置是通过表达式i = (n - 1) & hash计算的,可以认为这是一个随机的值。

4.最后,还是需要老生常谈地强调一下,HashMap不是线程安全的,其内部用于存放数据的容器本质上是一个一维数组,该数组本身并不是线程安全的,而且HashMap在写操作时也并未进行线程同步。如果需要使用线程安全的HashMap,应该使用ConcurrentHashMap,因为在其中用于存储数据的数组是线程安全的:

/**
 * The array of bins. Lazily initialized upon first insertion.
 * Size is always a power of two. Accessed directly by iterators.
 */
// ConcurrentHashMap内部存储数据的table通过关键字volatile修饰,因此是线程安全的
transient volatile Node<K,V>[] table;

原文地址:https://www.cnblogs.com/nuccch/p/9102013.html

时间: 2024-10-08 14:58:01

细说java系列之HashMap原理的相关文章

细说java系列之反射

什么是反射 反射机制允许在Java代码中获取被JVM加载的类信息,如:成员变量,方法,构造函数等. 在Java包java.lang.reflect下提供了获取类和对象反射信息的相关工具类和接口,如:Field,Method,Constructor 使用反射可以做什么事情 反射通常被用于需要检查或修改应用程序运行时行为的编程中,它是一个非常有用的技术. 具体来讲,可以在如下场景中使用反射机制: 功能扩展,应用程序可以通过反射创建一个具备完整限定名的类实例,从而使用一个外部的用户自定义的类. 在可视

Java集合类之HashMap原理小结

深入Java集合学习系列:HashMap的实现原理

参考文献 引用文献:深入Java集合学习系列:HashMap的实现原理,大部分参考这篇博客,只对其中进行稍微修改 自己曾经写过的:Hashmap实现原理 1. HashMap概述: HashMap是基于哈希表的Map接口的非同步实现(Hashtable跟HashMap很像,唯一的区别是Hashtalbe中的方法是线程安全的,也就是同步的).此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 2. HashMap的数据结构: 在ja

【JDK1.8】 Java小白的源码学习系列:HashMap

目录 Java小白的源码学习系列:HashMap 官方文档解读 基本数据结构 基本源码解读 基本成员变量 构造器 巧妙的tableSizeFor put方法 巧妙的hash方法 JDK1.8的putVal方法 JDK1.8的resize方法 初始化部分 数组搬移部分 Java小白的源码学习系列:HashMap 春节拜年取消,在家花了好多天时间啃一啃HashMap的源码,同样是找了很多很多的资料,有JDK1.7的,也有JDK1.8的,当然本文基于JDK1.8.将所学到的东西进行整理,希望回过头再看

Java 集合系列 09 HashMap详细介绍(源码解析)和使用示例

此页面为WP8"Surface Pro 3"应用的发布页面. "Surface Pro 3"是一款收集Surface Pro 3的玩机技巧的WP8程序,更好的帮助Surface用户理解并使用它. 此页面主要记录开发进度.APP发布等情况. -------------------相关进度--------------------- 目前进度:UI相关资源前期准备中,各相关开放平台的AppID申请中... Java 集合系列 09 HashMap详细介绍(源码解析)和使用

Java 集合系列 11 hashmap 和 hashtable 的区别

java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例 Java 集合系列 05 Vector详细介绍(源码解析)和使用示例 Java 集合系列 06 Stack详细介绍(源码解析)和使用示例 Java 集合系列 07 List总结(LinkedList, ArrayList等使用场景和

大神必修课系列之java 分布式架构的原理解析

分布式术语 1.1. 异常 服务器宕机 内存错误.服务器停电等都会导致服务器宕机,此时节点无法正常工作,称为不可用. 服务器宕机会导致节点失去所有内存信息,因此需要将内存信息保存到持久化介质上. 网络异常 有一种特殊的网络异常称为--网络分区 ,即集群的所有节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信. 磁盘故障 磁盘故障是一种发生概率很高的异常. 使用冗余机制,将数据存储到多台服务器. 1.2. 超时 在分布式系统中,一个请求除了成功和失败两种状态,还存在着超时状态. 可以

Java系列文章(全)

JVM JVM系列:类装载器的体系结构 JVM系列:Class文件检验器 JVM系列:安全管理器 JVM系列:策略文件 Java垃圾回收机制 深入剖析Classloader(一)--类的主动使用与被动使用 深入剖析Classloader(二)-根类加载器,扩展类加载器与系统类加载器 深入理解JVM-JVM内存模型 JVM-堆与栈 JVM调优总结-基本垃圾回收算法 JVM调优总结-垃圾回收面临的问题 JVM调优总结-分代垃圾回收详述 JVM架构解析 触发JVM进行Full GC的情况及应对策略 J

Java集合:HashMap源码剖析

一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调整大小 5.数据读取                       6.HashMap的性能参数                      7.Fail-Fast机制 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null