【Java】HashMap源码(1.7)

Life is not a ridiculous number of life, the meaning of life lies in life itself

HashMap源码

散列集

数组和链表可以保持元素插入的顺序,对数组来说,他的优点是拥有连续的存储空间,因此可以使用元素下标快速访问,但缺点在于如果要在数组中第n位删除或插入一个新元素,就需要移动n后面的所有元素,比如在ArrayList中删除某个元素就是调用系统的arraycopy方法将数组要删除的元素后面的所有元素向前复制一位得到的。

private void fastRemove(int index) {modCount++;int numMoved = size - index - 1;if (numMoved > 0)// 将elementData中从index + 1开始的numMoved长度的元素复制到elementData的index位置System.arraycopy(elementData, index+1, elementData, index,numMoved);elementData[--size] = null; // clear to let GC do its work
}

并且定义数组时必须指定容量,如果需要扩容就得重新申请一个更大的数组,然后把原来的数据复制到新数组中,

这就导致数组查询很快,但增删性能不高。

而对链表来说,它的内存空间不是连续的,也就不需要考虑容量问题,但这就导致链表的查询需要逐个遍历LinkedList中虽然可以通过索引来get元素,但也是从头部开始遍历的(如果索引大于size/2就从尾部遍历),效率很低。

散列集(hash table)可以说是数组与链表的组合,

UTOOLS1586163610224.png

往散列集中添加元素时,通过hash函数可以得到一个该元素的一个哈希值,Java中哈希值的范围在-2147483648~2147483647之间,Object类的hashCode()方法可以返回对象的哈希值,通过hashCode可以确定将该元素存到哪一个数组中,

不能直接使用hashCode,因为它的范围将近40亿,不可能有这么大的数组空间,所以需要对hashCode值做一定的处理,使之在数组容量范围内,最简单的办法是对数组容量取余,但取余有效率问题,所以Java使用了&操作,

如果key是null, 就返回0,否则返回原来哈希值与哈希值右移16位后的结果

比如一个元素的hashCode经过运算得到的值是5,他就会被放在第六个数组中。

应为数组容量是有限的,就一定存在运算后得到同样索引值的情况,称为哈希碰撞,解决哈希碰撞有两种方法:开放地址法拉链法 ,开放地址法是指如果当前的数组已经有元素了,就通过别的算法算出一个新位置插入,像python中dict的实现就使用了开放地址法;而Java中则使用了后者——拉链法,他的思路是如果当前位置有元素了,就把新元素链到旧元素上。

jdk 1.7 以及之前拉链使用一个链表实现,每次有冲突的新元素过来就会把新元素放到数组中,原来的旧链链接到新元素后面【头插法】;

jdk 1.8 开始加入了红黑树,如果数组某个位置的长度超过8并且数组容量超过32就会把链表转换为红黑树,如果红黑树经过删除节点数小于6,就会把树重新转换回链表,以此来提高效率。

JDK 1.7 中的实现

jdk 1.7 以及之前那个数组是Entry类型的,里面封装了key和value,也就是链表的一个节点。

static class Entry<K,V> implements Map.Entry<K,V> {final K key;V value;Entry<K,V> next;int hash;
}

基本属性

// 数组的默认大小,必须是2的倍数, 默认16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 数组最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;// 默认负载因子,如果数组中75%被占满,就要扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;// hashMap中数据的数量
transient int size;// 与快速失败有关
transient int modCount;

put方法

public V put(K key, V value) {if (table == EMPTY_TABLE) {// 初始化一个数组inflateTable(threshold);}// key为null的情况if (key == null)return putForNullKey(value);// 正常其他情况int hash = hash(key);int i = indexFor(hash, table.length);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;
}

如果当前table是空的的时候(实例化后第一次执行put),需要通过inflateTable()对哈希表进行初始化

private void inflateTable(int toSize) {// Find a power of 2 >= toSize// 计算实际的数组大小int capacity = roundUpToPowerOf2(toSize);// 计算出扩容的临界值thresholdthreshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);// 实例化一个新数组table = new Entry[capacity];initHashSeedAsNeeded(capacity);
}

由于数组容量要求是2的倍数,所以这个方法会先通过roundUpToPowerOf2()根据我们指定的数组容量计算出真实的数组容量capacity,然后实例化一个capacity大小的Entry数组。最后这个initHashSeedAsNeeded()允许你配置一个哈希种子,来手动影响散列结果。

初始化后,由于HashMap允许null作为key值,所以如果key是null,就执行putForNullKey()方法把null: value存入哈希表.

private V putForNullKey(V value) {// 遍历数组0位的链表for (Entry<K,V> e = table[0]; e != null; e = e.next) {// 如果数组0位链表某个节点key也是null,就替换该节点的值,返回旧值。if (e.key == null) {V oldValue = e.value;e.value = value;// 空方法e.recordAccess(this);return oldValue;}}// 如果0位没有key为null的节点,就创建新节点并加入链表modCount++;addEntry(0, null, value, 0);return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {// 如果HashMap中元素的数量大于临界值并且发生了冲突,就扩容if ((size >= threshold) && (null != table[bucketIndex])) {resize(2 * table.length);hash = (null != key) ? hash(key) : 0;bucketIndex = indexFor(hash, table.length);}// 创建新的Entry对象createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {// 原来的链表Entry<K,V> e = table[bucketIndex];// 实例化一个新的Entry对象,next指向旧的链表etable[bucketIndex] = new Entry<>(hash, key, value, e);// 元素个数加一size++;
}
  1. HashMap允许null作为key,并且这个元素始终放在数组第0位

回到正常情况,key是null就确定它存放在数组0位,但其他的key就需要通过计算得到index值,jdk1.7中首先在hash()方法中对对象原本的hashCode做一系列移位操作后,再在indexFor()方法中与数组长度做与运算得出对象最终应该被放在数组的哪一位。

final int hash(Object k) {// 可以设置环境变量来提供一个哈希种子int h = hashSeed;if (0 != h && k instanceof String) {return sun.misc.Hashing.stringHash32((String) k);}// 这个种子会通过与对象原来的hashCode做异或从而影响最终散列效果h ^= k.hashCode();h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {return h & (length-1);
}

出于性能的考虑,在获得最终的index时,Java采用了&操作而不是更简单的取余,这就导致数组长度必须是2的倍数,同时hash()方法中多次移位和异或也是应为这样。

比如一个字符串 “重地” 通过 hashCode()方法得到它原先的hashCode值为 1179395,假设数组没扩容,哈希种子是默认值0,那它计算index的过程应该是:

  1. 与hashSeed做异或,得到的还是它本身

  2. 右移20位的结果与右移12位的结果做异或

    h =         : 0000 0000 0001 0001 1111 1111 0000 0011 (1179395)
    a = h >>> 20: 0000 0000 0000 0000 0000 0000 0000 0001 (1)
    b = h >>> 12: 0000 0000 0000 0000 0000 0001 0001 1111‬ (287)
    ----------------------------------------------------------------
    a ^ b =     : 0000 0000 0000 0000 0000 0001 0001 1110 (286)
    h =         : 0000 0000 0001 0001 1111 1111 0000 0011 (1179395)
    ----------------------------------------------------------------
    h =         : 0000 0000 0001 0001 1111 1110 0001 1101 (1179165)
    c = h >>> 7 : 0000 0000 0000 0000 0010 0011 1111 1100
    ----------------------------------------------------------------
    h ^ c =     : 0000 0000 0001 0001 1101 1101 1110 0001
    d = h >>> 4 : 0000 0000 0000 0001 0001 1101 1101 1110
    ----------------------------------------------------------------
    h ^ d =     : 0000 0000 0001 0000 1100 0000 0011 1111
    len - 1     : 0000 0000 0000 0000 0000 0000 0000 1111
    ----------------------------------------------------------------
    index &     : 0000 0000 0000 0000 0000 0000 0000 1111
    

    到最后发现,真正参与运算的只有低四位,之所以做多次位移和异或运算,就是为了把hashCode的高位也参与到最后的与运算中,让得到的index尽量分散,如果把最高位用A表示,可以看到经过上面的算法,最高位究竟影响了哪些位置:

    h =         : A000 0000 0001 0001 1111 1111 0000 0011 (1179395)
    a = h >>> 20: 0000 0000 0000 0000 000A 0000 0000 0001 (1)
    b = h >>> 12: 0000 0000 0000 A000 0000 0001 0001 1111‬ (287)
    ----------------------------------------------------------------
    a ^ b =     : 0000 0000 0000 A000 000A 0001 0001 1110 (286)
    h =         : 0000 0000 0001 0001 1111 1111 0000 0011 (1179395)
    ----------------------------------------------------------------
    h =         : 0000 0000 0001 A001 111A 1110 0001 1101 (1179165)
    c = h >>> 7 : 0000 0000 0000 0000 001A 0011 11A1 1100
    ----------------------------------------------------------------
    h ^ c =     : 0000 0000 0001 A001 110A 1101 11A0 0001
    d = h >>> 4 : 0000 0000 0000 0001 A001 110A 1101 11A0
    ----------------------------------------------------------------
    h ^ d =     : 0000 0000 0001 A000 110A 000A 00A1 11A1
    len - 1     : 0000 0000 0000 0000 0000 0000 0000 1111
    ----------------------------------------------------------------
    index &     : 0000 0000 0000 A000 000A 000A 00A0 11A1
    

    最高位最后影响了低四位。

    为什么数组容量要是2的倍数

    让与运算之后的结果分布在 0 ~ (len -1) 之间

算出index之后的代码逻辑就和putForNullKey差不多了,唯一的区别在于:

if (e.hash == hash && ((k = e.key) == key || key.equals(k))){...}

这样设计的原因在于:

  • 哈希值不同一定不是同一个对象
  • 同一个对象哈希值不一定相同

扩容

是否扩容的判断在addEntry方法中,如果满足扩容条件,是先扩容,再添加新元素

    void addEntry(int hash, K key, V value, int bucketIndex) {if ((size >= threshold) && (null != table[bucketIndex])) {// 2倍扩容resize(2 * table.length);hash = (null != key) ? hash(key) : 0;bucketIndex = indexFor(hash, table.length);}createEntry(hash, key, value, bucketIndex);}

扩容需要满足两个条件:

  1. HashMap中元素个数大于等于threshold
  2. 即将要新插入的元素发生了冲突

第一个条件 size是总元素个数,但threshold是根据数组容量算的。

threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
void resize(int newCapacity) {// 得到旧数组的引用Entry[] oldTable = table;int oldCapacity = oldTable.length;// 如果旧数组已经不能再长了,就不扩容了if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}// 创建一个2倍旧数组大小的新数组Entry[] newTable = new Entry[newCapacity];// 将旧数组的元素转移到新数组transfer(newTable, initHashSeedAsNeeded(newCapacity));table = newTable;// 重新计算扩容临界值threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

扩容最核心的就是数据转移,也就是transfer()方法

void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;// 第一重循环遍历数组for (Entry<K,V> e : table) {// 第二重循环遍历链表while(null != e) {Entry<K,V> next = e.next;if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);// 到这又变成了尾插e.next = newTable[i];newTable[i] = e;e = next;}}
}

由于数组容量变了两倍,所以index也许需要重新计算,但计算中其实前面的步骤都一样,只不过最后一步时 length - 1 在最前面多了一个1,所以哪怕index值改变,变化后的index与原来的也是2的倍数关系(1.8中用到了这个规律)

扩容过程中出现的循环链表的情况

UTOOLS1586269660885.png

这是两个线程进入transfer后一开始的情况(两个线程现在都有了自己新的数组),如果线程1正常执行完成,线程2阻塞在Entry<K,V> next = e.next;之后,那结果就是:

UTOOLS1586274296957.png

然后线程2开始执行

UTOOLS1586274868757.png

就出现了循环链表的情况。

参考

参考2

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/457472.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker 基本用法

1.安装&#xff1a; wget http://mirrors.yun-idc.com/epel/6/i386/epel-release-6-8.noarch.rpm rpm -ivh epel-release-6-8.noarch.rpm yum install docker-io -y2.获取镜像 pull docker pull ubuntu docker pull ubuntu:14.043.运行这个镜像&#xff0c;在其中运行bash应用…

画刷的使用

1.画刷的定义&#xff1a; HBRUSH hBrush; windows 自定义的画刷&#xff1a; WHITE_BRUSH、LTGRAY_BRUSH、GRAY_BRUSH、DKGRAY_BRUSH、BLACK_BRUSH和NULL_BRUSH &#xff08;也叫HOLLOW_BRUSH&#xff09; 获取方法如下&#xff1a; hBrush (HBRUSH) GetStockObject (GRAY_BR…

dataframe 控对象_iOS知识 - 常用小技巧大杂烩

1&#xff0c;打印View所有子视图po [[self view]recursiveDescription]2&#xff0c;layoutSubviews调用的调用时机* 当视图第一次显示的时候会被调用。* 添加子视图也会调用这个方法。* 当本视图的大小发生改变的时候是会调用的。* 当子视图的frame发生改变的时候是会调用的。…

【Java】jdk 1.8 新特性——Lambda表达式

Lambda表达式 jdk 1.8 新加入的特性&#xff0c;简化了简单接口的实现 函数式接口 函数式中只有一个待实现的方法&#xff0c;可以使用FunctionalInterface注解标注函数式接口.这个接口中只能有一个待实现的方法&#xff0c;但可以包含默认方法&#xff0c;静态方法以及Obje…

【Todo】Java8新特性学习

参考这篇文章吧&#xff1a; http://blog.csdn.net/vchen_hao/article/details/53301073 还有一个系列转载于:https://www.cnblogs.com/charlesblc/p/6123380.html

jsp调整字体大小font_html font标签如何设置字体大小?

首先我们先来看看htmlfont标签是如何来设置字体大小的&#xff1a;都只到htmlfont标签是个专门用来设置字体的标签&#xff0c;虽然在html5中用的会很少(因为都用css样式来设置font标签里面的属性)&#xff0c;但是个人觉得font标签还是相当强大的标签的&#xff0c;为什么这么…

runtime官方文档

OC是一种面向对象的动态语言&#xff0c;作为初学者可能大多数人对面向对象这个概念理解的比较深&#xff0c;而对OC是动态语言这一特性了解的比较少。那么什么是动态语言&#xff1f;动态语言就是在运行时来执行静态语言的编译链接的工作。这就要求除了编译器之外还要有一种运…

【Java】synchronized关键字笔记

Java Synchronized 关键字 壹. Java并发编程存在的问题 1. 可见性问题 可见性问题是指一个线程不能立刻拿到另外一个线程对共享变量的修改的结果。 如&#xff1a; package Note.concurrency;public class Demo07 {private static boolean s true;public static void mai…

sql语句分析是否走索引_MySql 的SQL执行计划查看,判断是否走索引

在select窗口中&#xff0c;执行以下语句&#xff1a;set profiling 1; -- 打开profile分析工具show variables like %profil%; -- 查看是否生效show processlist; -- 查看进程use cmc; -- 选择数据库show PROFILE all; -- 全部分析的类型show index from t_log_account; ##查看…

SQL Server-数据类型(七)

前言 前面几篇文章我们讲解了索引有关知识&#xff0c;这一节我们再继续我们下面内容讲解&#xff0c;简短的内容&#xff0c;深入的理解&#xff0c;Always to review the basics。 数据类型 SQL Server支持两种字符数据类型&#xff0c;一种是常规&#xff0c;另外一种则是Un…

【随记】SQL Server连接字符串参数说明

废话不多说&#xff0c;请参见 SqlConnection.ConnectionString 。 转载于:https://www.cnblogs.com/xiesong/p/5749037.html

【设计模式 00】设计模式的六大原则

设计模式的六大原则 参考&#xff1a; 设计模式六大原则 1. 单一职责原则 一个类只负责一个明确的功能 优点&#xff1a; 降低类的复杂度&#xff0c;提高代码可读性和可维护性降低变更时对其他功能的影响 2. 里氏替换原则 **原则一&#xff1a;**若 o1 是 C1 的一个实例化…

pb retrieve时停止工作_大佬们挂在嘴边的PE、PB是什么?

在紧锣密鼓地准备科创50ETF的发行工作间隙&#xff0c;今天小夏先带你读懂最简单的PE、PB估值指标这两大指标。01、什么是PE&#xff08;市盈率&#xff09;PE&#xff0c;也就是市价盈利比率&#xff0c;简称市盈率。市盈率是指股票价格与每股收益&#xff08;每股收益&#x…

EF CodeFirst 如何通过配置自动创建数据库当模型改变时

最近悟出来一个道理&#xff0c;在这儿分享给大家&#xff1a;学历代表你的过去&#xff0c;能力代表你的现在&#xff0c;学习代表你的将来。 十年河东十年河西&#xff0c;莫欺少年穷 学无止境&#xff0c;精益求精 本篇为进阶篇&#xff0c;也是弥补自己之前没搞明白的地方,…

对AutoIt中控件和窗口的理解

经过尝试&#xff0c;对AutoIt中Control和Window有了新的认识&#xff0c;分享一下 1.Control 现在我想对一个WinForm架构的应用程序进行自动化操作&#xff0c;得到控件Advanced Mode属性为[Name:XXX]。 然而在该窗口中有多个相同属性的Control&#xff0c;而依该属性只能操作…

【设计模式 01】简单工厂模式(Simple factory pattern)

简单工厂模式 可以根据参数的不同返回不同类的实例 参考&#xff1a; CSDN|简单工厂模式 简单工厂通过传给工厂类的参数的不同&#xff0c;返回不同的对象&#xff0c;包括三部分组成&#xff1a; 具体的”产品“工厂类&#xff08;实例化并返回”产品“&#xff09;客户端&am…

[Hadoop]MapReduce多路径输入与多个输入

1. 多路径输入 FileInputFormat是所有使用文件作为其数据源的 InputFormat 实现的基类&#xff0c;它的主要作用是指出作业的输入文件位置。因为作业的输入被设定为一组路径&#xff0c; 这对指定作业输入提供了很强的灵活性。FileInputFormat 提供了四种静态方法来设定 Job 的…

pvrect r语言 聚类_R语言实现KEGG通路富集可视化

用过KEGG的朋友应该都很熟悉里面的通路地图。你是否想过如果自己可以控制通路图将自己的基因绘制在一个通路图中&#xff0c;那么今天给大家介绍一个新推出的Bioconductor软件包pathview。这个包可以进行KEGG富集分析。首先&#xff0c;我们不耐烦的介绍下Bioconductor包的安装…

【设计模式 02】策略模式( Strategy)

策略模式 参考&#xff1a; CSDN | 策略模式百家号 | 策略模式 如果某个系统需要不同的算法&#xff08;如超市收银的优惠算法&#xff09;&#xff0c;那么可以把这些算法独立出来&#xff0c;使之之间可以相互替换&#xff0c;这种模式叫做策略模式&#xff0c;它同样具有三个…

PL/SQL复合变量

复合变量可以将不同数据类型的多个值存储在一个单元中。由于复合类型可以由用户自己根据需要定义其结构&#xff0c;所以复合数据类型也称为自定义数据类型。在PL/SQL中&#xff0c;使用%TYPE声明的变量类型与数据表中字段的数据类型相同&#xff0c;当数据表中字段数据类型修改…