1. 简介

Java8 HashMap结构(数组 + 列表 + 红黑树)如图:

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。

2. 定义

2.1 主要属性

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//序列号,序列化的时候使用。
private static final long serialVersionUID = 362498820763181265L;
/**默认容量,1向左移位4个,00000001变成00010000,也就是2的4次方为16,使用移位是因为移位是计算机基础运算,效率比加减乘除快。**/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子,用于扩容使用。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当某个桶节点数量大于8时,会转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;
//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6;
//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,transient关键字表示该属性不能被序列化
transient Node<K,V>[] table;
//将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
transient Set<Map.Entry<K,V>> entrySet;
//元素数量
transient int size;
//统计该map修改的次数
transient int modCount;
//临界值,也就是元素数量达到临界值时,会进行扩容。
int threshold;
//也是负载因子,只不过这个是变量。
final float loadFactor;
}

2.2 构造方法

HashMap共有三个构造函数:

初始化一个默认容量=16,负载因子=0.75 的hashmap对象。

public HashMap() {
// DEFAULT_LOAD_FACTOR = 0.75f
this.loadFactor = DEFAULT_LOAD_FACTOR;
}

初始化一个指定初始容量和负载因子-0.75 的hashmap对象。

public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

初始化一个指定初始容量和负载因子 的HashMap对象。

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

2.3 node

Node是HashMap的静态内部类,HashMap主干是一个Node数组,Node是HashMap的最基本组成单位。

/**
* HashMap 的Node 节点元素
* @param <K> 元素的key
* @param <V> 元素的Value
*/
static class Node<K,V> implements Map.Entry<K,V> {
// 这个节点所在位置的hash值
final int hash;
//这个节点的Key
final K key;
//这个节点的value
V value;
//后继节点
Node<K,V> next;
//构造方法
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
} public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; } /**
* 获取HashCode
* key和value 的hash做异或运算 防止hash冲突
* @return
*/
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
} /**
* 设置value
* @param newValue
* @return
*/
public final V setValue(V newValue) {
V oldValue = value;
//替换当前node的value
value = newValue;
//返回旧的value
return oldValue;
} /**
* equals 比较
* 如果 key和value都一致 判断equals相等
* @param o
* @return
*/
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

3. 初始容量 负载因子

上文中反复提到了两个参数:初始容量,负载因子。这两个参数是影响HashMap性能的重要参数。

容量:transient Node<K,V>[] table; 即 table的长度,初始容量是创建哈希表时的容量 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 即初始容量为 16

负载因子: final float loadFactor; 容器进行初始化的时候会将值设置为0.75 ( 也就是初始可用的容量为:16 * 0.75 = 12,当容量达到12的时候就会进行扩容操作),负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间使用程度,负载因子越大表示散列表的装填程度越高,反之越小。

为什么说 容量 和 负载因子 会影响 HashMap的性能?

我们在考虑HashMap的时候,首先要想到HashMap只是一个数据结构,既然是数据结构最主要的就是节省时间和空间。负载因子的作用肯定是节省时间和空间。为什么节省呢?

  1. 假设 负载因子 = 1.0

    HashMap是将key进行hash运算得到桶的位置(table的索引)的。既然是hash运算,那么Hash冲突是避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash冲突(因为要将整个table填满,并且为了将数均匀填充,jdk还使用了扰动函数,增加随机性),底层的红黑树会变的异常复杂。对查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。

  2. 假设 负载因子 = 0.5

    负载因子是0.5的时候,也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素好了,Hash冲突也会减少,那么底层的链表或者红黑树的高度就会降低。查询效率就会增加。但是,这时候空间利用率就会大大降低,显然也不太好。

总结:

​ 默认容量 = 16,负载因子 = 0.75,这两个常量的值都是经过大量的计算和统计得出来的最优解。

当然 如果知道自己的hashmap容量大小,尽量在初始化的时候就指定一下,可以避免扩容带来的性能损耗。但负载因子就别随意改了,毕竟是最优解。

4. 添加元素

put方法是一个重点方法,这里有hashmap的初始化,数据的在hashmap中是如何存储的,什么情况下会转换成红黑树等。

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
} /**
* putVal 方法 真正进行插入操作的方法,
*
* @param hash 传入key的哈希值
* @param key
* @param value
* @param onlyIfAbsent 如果该值是true,如果存在值就不会进行修改操作
* @param evict LinekdHashMap尾操作使用,这里暂无用途
* @return
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
/**********初始化********/ // 如果table长度是0或table是null会调整一次大小
// 这时tab会指向调整大下后的Node<K,V>[](主干数组)
// n被赋值为新数组长度 // 如果没有调整大小,tab指向table
if ((tab = table) == null || (n = tab.length) == 0) {
n = (tab = resize()).length;
}
/********开始查找键的位置,并存储value*******/
// i = (n - 1) & hash这个是获取key应该在哪个桶里,下面详说
// 这里将p指向当前key所需要的那个桶
if ((p = tab[i = (n - 1) & hash]) == null) {
// 如果空桶,也就是无哈希冲突的情况,直接丢个Node进去。
// 此时的tab就是table
tab[i] = newNode(hash, key, value, null);
//存在冲突,开始寻找我们要找的节点
} else {
Node<K, V> e;
K k;
// 判断第一个节点是不是我们找的
// 此时k储存了 p.key
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
// hash值相等,key值相等,定位完成,是修改操作
// e来储存p这个节点,一会修改
e = p;
// 判断是否是红黑树节点
} else if (p instanceof TreeNode) {
// 是红黑树节点,存在就返回那个节点,不存在就返回null
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
// 最终,是链表了,开始对链表遍历查找
} else {
for (int binCount = 0; ; ++binCount) {
// 上面知道第一个接点不是我们要的,直接获取下一个,并储存给e
// 下一个是空,直接丢个Node在这里,然后p.next指向这里
// 这里下一个节点地址给了e
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// !大于树化阀值,开始树化
// 注意-1是因为binCount是索引而不是长度
// 其实此时链表长度已经是7+1(索引) + 1(新进来的Node)
// 已经大于树化阀值8,也就是说链表长度为8时是不会树化的
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;
}
}
// 上面说了,这有修改操作e才能不是null
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 给e新值
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
// 这个是LinkedHashMap用的,HashMap里是个空实现
afterNodeAccess(e);
// 修改就会把旧值返回去
return oldValue;
}
}
/*********修改完成的后续操作**********/
// 修改次数加1
++modCount;
// 如果size大于阀值,会执行resize()方法调整大小
if (++size > threshold) {
resize();
}
// 这个是给LinkedHashMap用的,HashMap里也是个空实现
afterNodeInsertion(evict);
// 添加成功返回null
return null;
}

hash方法 扰动函数

/**
* hash 运算
* @param key
* @return
*/
static final int hash(Object key) {
int h;
/**
* key是null就返回0,key不是null就先取hashCode()
* 然后与这个hashCode()无符号右移进行亦或运算
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

看到了熟悉的hashCode,这就解释了为什么重写equals方法的时候,一定要重写hashCode方法,因为key是基于hashCode来处理的。

为什么 获取了key的hashcode() 返回的int型的散列值还要异或(^)h >>> 16 呢? 有什么用?

实质上是把一个数的低16位与它的高16位做异或运算,混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

**那为什么要增加低16位的随机性呢? **

根本目的是为了增加 散列表的装填程度,为了使数据分布的更均匀。

因为在找key的位置tab[i = (n - 1) & hash]),是通过(n - 1) & hash 计算索引位置的,而当n的长度不够大时,只和hashCode()的低16位有关。

这样做有几个好处:

  1. &运算速度快,至少比%取模运算快
  2. 能保证 索引值 肯定在 capacity 中,不会超出数组长度
  3. ( n -1) & hash,当n为2次幂时,会满足一个公式:(n -1) & hash = hash % n

5. 扩容方法

扩容的三种情况:

  1. 使用默认构造方法初始化HashMap。从前文可以知道HashMap在一开始初始化的时候会返回一个空的table,并且thershold为0。因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。
  2. 指定初始容量的构造方法初始化HashMap。那么从下面源码可以看到初始容量会等于threshold,接着threshold = 当前的容量(threshold) * DEFAULT_LOAD_FACTOR。
  3. HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍

这边也可以引申到一个问题就是HashMap是先插入数据再进行扩容的,但是如果是刚刚初始化容器的时候是先扩容再插入数据。

5.1 扩容部分

/**
* 扩容方法
*
* @return
*/
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = table;
// 原容量,table为null返回0,否则返回table长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//原始阈值
int oldThr = threshold;
//新容量,新阈值
int newCap, newThr = 0;
// table已经初始化,旧容量>0
if (oldCap > 0) {
// 容量已经超过最大容量,直接返回去
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
// 2倍扩容后小于最大容量,并且原容量大于默认初始化容量(我还没想清楚为什么要大于默认初始容量)
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
// 阀值加倍
newThr = oldThr << 1; // double threshold
}
// 原数组容量为0,未初始化,但阀值不为0
// 也就是构造方法里threshold = tableSizeFor(initialCapacity)这个步骤
} else if (oldThr > 0) { // initial capacity was placed in threshold
newCap = oldThr;
// 啥都没有,默认构造
}else { // zero initial threshold signifies using defaults
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;

5.2 复制数据部分

// 创建新的数组
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; table = newTab;
//开始复制数据
if (oldTab != null) {
//开始遍历
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
// 获取桶的第一个节点
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//没有后继节点,说明为空,直接移过去
if (e.next == null) {
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树,分裂放入新数组
} else if (e instanceof TreeNode) {
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
//链表操作在下面
} else { // preserve order
// 不是直接进行计算元素在新数组中的位置,而是原位置加原数组长度
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
// 把链表下一个节点放在 next里
next = e.next;
// 该节点不需要移动
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);
// 把两个首元素放在两个桶里就可以了
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新的数组
return newTab;

Java-基础-HashMap的更多相关文章

  1. Java基础-hashMap原理剖析

    Java基础-hashMap原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.   一.什么是哈希(Hash) 答:Hash就是散列,即把对象打散.举个例子,有100000条数 ...

  2. java基础---->hashMap的简单分析(一)

    HashMap是一种十分常用的数据结构对象,可以保存键值对.它在项目中用的比较多,今天我们就来学习一下关于它的知识. HashMap的简单使用 一.hashMap的put和get方法 Map<S ...

  3. java基础hashmap

    Iterator中hasNext(), next() 在Iterator类中,我们经常用到两个方法: hasNext(), next(),具体含义: next(),  是返回当前元素, 并指向下一个元 ...

  4. Java基础——HashMap

    1.HashMap底层的实现 JDK 1.7 中 HashMap 是以数组+链表的形式组成的 JDK 1.8 之后数组+链表/红黑树的组成的,当链表大于 8 并且容量大于 64 时,链表结构会转换成红 ...

  5. java 基础 HashMap 并发扩容问题

    存入的数据过多的时候,尤其是需要扩容的时候,在并发情况下是很容易出现问题. resize函数: void resize(int newCapacity) { Entry[] oldTable = ta ...

  6. java基础-java核心知识库

    本人从事java开发6年左右,主要从事互联网相关的开发,目前还是奋战在一线的码农,痛并快乐着.受互联网产品热潮的影响,关注高性能低成本架构,互联网开发框架,以下是我认为作为一个资深java程序员应该掌 ...

  7. Java基础知识强化之集合框架笔记62:Map集合之HashMap嵌套HashMap

    1. HashMap嵌套HashMap  传智播客          jc    基础班                      陈玉楼  20                      高跃   ...

  8. java基础系列(三)---HashMap

    java基础系列(三)---HashMap java基础系列 java基础系列(一)---String.StringBuffer.StringBuilder java基础系列(二)---Integer ...

  9. java基础解析系列(五)---HashMap并发下的问题以及HashTable和CurrentHashMap的区别

    java基础解析系列(五)---HashMap并发下的问题以及HashTable和CurrentHashMap的区别 目录 java基础解析系列(一)---String.StringBuffer.St ...

  10. Java基础系列--HashMap(JDK1.8)

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/10022092.html Java基础系列-HashMap 1.8 概述 HashMap是 ...

随机推荐

  1. 谷歌浏览器chrome安装插件报"程序包无效: CRX_HEADER_INVALID"错误

    今天参加需求评审,看到原来可以谷歌浏览器查看Axure原型文件,真是只有想不到,没有做不到(自己孤陋寡闻了,第一次接触Axure). 需求评审后,我百度"如何使用谷歌浏览器查看Axure原型 ...

  2. 修改MAC系统下默认PHP版本(解决自带版本和环境版本冲突)

    https://www.jianshu.com/p/d080d06557be 更改环境变量来修改默认的php版本 新建一个.bas_profile文件并编辑 vim ~/.bash_profile 然 ...

  3. java 工具类 验证码

    第一步: 引入工具类 工具类一: import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import jav ...

  4. Python3入门系列之-----file方法操作

    file方法 file处理文件的一些方法,创建一个file对像后即可对文件进行读写相关操作,首先你得打开文件,此处用到open函数 open函数 语法:file_objcet = open(file_ ...

  5. 寻找写代码感觉(五)之Mybatis官方代码生成器的使用

    一.Mybatis Generator生成器 见名知意,官方给出的代码生成器.好处就是不用自己写实体类.接口.xml文件了,应对简单增删改查是可以的.复杂的还是需要自己手写sql的. 二.Mybati ...

  6. dbus客户端使用指南

    DBus是Linux使用的进程间通信机制,允许各个进程互相访问,而不需要为每个其他组件实现自定义代码.即使对于系统管理员来说,这也是一个相当深奥的主题,但它确实有助于解释linux的另一部分是如何工作 ...

  7. NOI2016区间bzoj4653(线段树,尺取法,区间离散化)

    题目描述 在数轴上有 \(N\) 个闭区间 \([l_1,r_1],[l_2,r_2],...,[l_n,r_n]\) .现在要从中选出 \(M\) 个区间,使得这 \(M\) 个区间共同包含至少一个 ...

  8. 路由器的不同接口对WANsim的影响

    随着网络的快速发展,移动设备已经成为我们日常生活中不可或缺的一部分.人们习惯用手机看新闻.看视频.点外卖.打车.购物等等. 同时,广域网也为移动通讯带来了挑战.以视频流来举例,从用户终端到达服务器,这 ...

  9. 高效动画实现原理-Jetpack Compose 初探索

    一.简介 Jetpack Compose是Google推出的用于构建原生界面的新Android 工具包,它可简化并加快 Android上的界面开发.Jetpack Compose是一个声明式的UI框架 ...

  10. NKOJ-4573 Falsita

    问题描述: 到海边了呢...... 如果没有那次选择,现在是不是会好些呢...... 都过去了. 仰望着星空,迎面吹过一阵阵海风,倚靠着护栏,Fine 在海边静静地伫立着,在一个个无际的长夜后,Fin ...