我的编程空间,编程开发者的网络收藏夹
学习永远不晚

java集合类HashMap源码解析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

java集合类HashMap源码解析

在这里插入图片描述

Map集合

Map集合存储的是键值对

Map集合的实现类:

HashTableLinkedHashMapHashMapTreeMap

HashMap

基础了解

1、键不可以重复,值可以重复;

2、底层使用哈希表实现;

3、线程不安全;

4、允许key为null,但只允许有一条记录为null,value也可以为null,允许多条记录为null;

源码分析

(一)以JDK1.7为例

1、存储结构

在这里插入图片描述

数据结构:数组+链表

首先hashmap内部有一个Entry类型的数组table;

通过Entry<K,V> 知道table数组每一个节点,存储的元素是键值对;

再通过字段next知道,每一个节点当出现哈希冲突的时候,会通过链表的形式将哈希值相同的节点放在同一个桶内;

四个字段:K,V,next,hash;


static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    public final K getKey() {
        return key;
    }
    public final V getValue() {
        return value;
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    public final String toString() {
        return getKey() + "=" + getValue();
    }
}

2、拉链法的工作原理


HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");

新建一个 HashMap,默认大小为 16;

插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。

插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。

插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。

应该注意到链表的插入是以头插法方式进行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 后面,而是插入在链表头部。

查找需要分成两步进行:

计算键值对所在的桶;

在链表上顺序查找,时间复杂度显然和链表的长度成正比

在这里插入图片描述

3、put()方法

put方法调用

1.调用hash函数得到key的HashCode值

2.通过HashCode值与数组长度-1逻辑与运算得到一个index值

3.遍历索引位置对应的链表,如果Entry对象的hash值与hash函数得到的hash值相等,并且该Entry对象的key值与put方法传过来的key值相等则,将该Entry对象的value值赋给一个变量,将该Entry对象的value值重新设置为put方法传过来的value值。将旧的value返回。

4.添加Entry对象到相应的索引位置


public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 键为 null 单独处理
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    // 确定桶下标
    int i = indexFor(hash, table.length);
    // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    // 插入新键值对
    addEntry(hash, key, value, i);
    return null;
}

HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对


private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。


void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    // 头插法,链表头部指向新的键值对
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}

4、确定桶下标

很多操作都需要先确定一个键值对所在的桶下标。如上所示代码


int hash = hash(key);
int i = indexFor(hash, table.length);

4.1、确定hash值


final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}

4.2、取模确定桶下标

令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:


x   : 00010000
x-1 : 00001111

令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:


y       : 10110010
x-1     : 00001111
y&(x-1) : 00000010

这个性质和 y 对 x 取模效果是一样的:


y   : 10110010
x   : 00010000
y%x : 00000010

我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。

确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算。


static int indexFor(int h, int length) {
    return h & (length-1);
}

面试题目:hashmap的初始容量值为什么设置为16?

原因1、根据确定桶下标的原理, h & (length-1),长度length为2的整数次幂可以保证散列的均匀,提升效率;

原因2、因为length为偶数,length-1必为奇数,所以h值的奇偶数决定了散列表数组落入奇数或者偶数数组内;这样保证了散列的均匀性。而如果length为奇数,那么length-1位偶数,最后一位为0,根据 逻辑 & 的原则码,最后一位肯定都是偶数0,而不可能出现奇数1,所以散列表只能使用一半的数组,造成很大的浪费;

5、扩容原理

HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!

在这里插入图片描述


static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;

从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍


void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。

多线程下扩容会出现HashMap的循环链表情况


void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
    Entry[] class="lazy" data-src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < class="lazy" data-src.length; j++) {
        Entry<K,V> e = class="lazy" data-src[j];
        if (e != null) {
            class="lazy" data-src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

6、扩容-重新计算桶下标

在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。

假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:


capacity     : 00010000
new capacity : 00100000
对于一个 Key,

它的哈希值如果在第 5 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 +16。

7、计算数组容量

HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。

先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:


mask |= mask >> 1    11011000
mask |= mask >> 2    11111110
mask |= mask >> 4    11111111

mask+1 是大于原始数字的最小的 2 的 n 次方。


num     10010000
mask+1 100000000

以下是 HashMap 中计算数组容量的代码:


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;
}

8、JDK1.8开始,链表转换为红黑树

从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。

数据结构:数组+链表+红黑树

在这里插入图片描述

get()

get方法调用

1.当调用get方法时会调用hash函数,这个hash函数会将key的hashCode值返回,返回的hashCode与Entry数组长度-1进行逻辑与运算得到一个index值,用这个index值来确定数据存储在Entry数组当中的位置

2.通过循环来遍历索引位置对应的链表,初始值为数据存储在Entry数组当中的位置,循环条件为Entry对象不为null,改变循环条件为Entry对象的下一个节点

3.如果hash函数得到的hash值与Entry对象当中key的hash值相等,并且Entry对象当中的key值与get方法传进来的key值equals相同则返回该Entry对象的value值,否则返回null

在这里插入图片描述

我们能否让HashMap同步?

在多线程条件下,容易导致死循环,具体表现为CPU使用率100%。因此多线程环境下保证 HashMap 的线程安全性,主要有如下几种方法:

1、使用 java.util.Hashtable 类,此类是线程安全的。

2、使用 java.util.concurrent.ConcurrentHashMap,此类是线程安全的。

3、使用 java.util.Collections.synchronizedMap() 方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。

通过Collections.synchronizedMap()来封装所有不安全的HashMap的方法,就连toString, hashCode都进行了封装. 封装的关键点有2处

(1)使用了经典的synchronized来进行互斥,

(2)使用了代理模式new了一个新的类,这个类同样实现了Map接口

到此这篇关于HashMap集合类的写法的文章就介绍到这了,更多相关Java遍历HashMap内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

java集合类HashMap源码解析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

java集合类源码分析之Set详解

Set集合与List一样,都是继承自Collection接口,常用的实现类有HashSet和TreeSet。值得注意的是,HashSet是通过HashMap来实现的而TreeSet是通过TreeMap来实现的,所以HashSet和TreeS
2023-05-31

Java HashMap使用源码分析

这篇文章主要讲解了“Java HashMap使用源码分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java HashMap使用源码分析”吧!0. 成员变量首先我们先看一下 HashMap
2023-07-06

Java源码角度分析HashMap用法

—HashMap—优点:超级快速的查询速度,时间复杂度可以达到O(1)的数据结构非HashMap莫属。动态的可变长存储数据(相对于数组而言)。缺点:需要额外计算一次hash值,如果处理不当会占用额外的空间。—HashMap如何使用—平时我们
2023-05-30

Java集合HashMap的知识点详解

这篇文章主要讲解了“Java集合HashMap的知识点详解”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java集合HashMap的知识点详解”吧!一、什么是哈希表在讨论哈希表之前,我们先大
2023-06-02

分析Java中HashMap集合的常用方法

这篇文章主要介绍“分析Java中HashMap集合的常用方法”,在日常操作中,相信很多人在分析Java中HashMap集合的常用方法问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”分析Java中HashMap集
2023-06-25

如何用源码分析Java HashMap实例

本篇文章为大家展示了如何用源码分析Java HashMap实例,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。引言HashMap在键值对存储中被经常使用,那么它到底是如何实现键值存储的呢?一 Entr
2023-06-17

【LinkedHashMap】| 深度剥析Java SE 源码合集Ⅴ

目录 1. 概述2. 类图3. 属性4. 构造方法5. 创建节点6. 节点操作回调6.1 afterNodeAccess6.2 afterNodeInsertion6.3 afterNodeRemoval 7. 转换成数组8.
2023-08-18

Java并发源码分析ConcurrentHashMap线程集合

这篇文章主要为大家介绍了Java并发源码分析ConcurrentHashMap线程集合,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-02-01

如何解读Java HashMap核心源码

这期内容当中小编将会给大家带来有关如何解读Java HashMap核心源码,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。对HashMap实现的源码进行简单的分析。 所使用的HashMap源码的版本信息如下
2023-06-17

Java集合框架概览之ArrayList源码分析

今天小编给大家分享一下Java集合框架概览之ArrayList源码分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。一、从一
2023-07-05

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录