- 简单谈谈HashMap
- 前言
- hash算法
- 常用的哈希算法
- hashmap的数据结构
- jdk1.7和jdk1.8的区别
- 简单讲解过程
- 1.8之后为什么要引入红黑树?
- 什么时候扩容?
- 扩容源码
- 高频集合扩容的信息
- 什么时候转红黑树?
- HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?
- 当两个对象的 hashCode 相等时会怎么样?
- 什么是哈希碰撞,如何解决哈希碰撞?
- 如果两个键的 hashCode 相同,如何存储键值对?
- HashMap集合类的成员
- 1.序列化版本号
- 2.集合的初始容量
- 那我自定义给的容量不是2的N次方呢?
- 3.负载因子(加载因子)
- 4.集合最大容量
- 5.树化阈值(转红黑树的边界值)
- 6.取消数化阈值(树转链表)
- HashMap的构造方法
- 1.默认
- 2.指定容量
- 3.指定容量和负载因子
- 4.指定集合构造器
- HashMap的成员方法
- 1.增加put
- 2.核心增加putVal
- 数组table的创建
- 数组的索引的算法
- 3.转化红黑树treeifyBin
- 什么是2-3-4树?
- 2-3-4树和红黑树的关系
- 什么是红黑树?
- 红黑树对于插入节点的分析
- 红黑树对于删除节点的分析
- 转换方法treeifyBin
- 4.扩容resize
- 容量翻倍
- 重新计算,并旧转新
- 扩容方法中JDK8对JDK7的优化
- 5.删除remove
- 6.核心删除removeNode
- 7.获取get
- 8.核心获取getNode
- 遍历hashmap的5种方式
- 1.循环遍历map自带的entrySet中的entry
- 2.ForEach迭代键值对方式
- 3.带泛型的map的entrySet的迭代器进行遍历
- 4.不带泛型的map的entrySet的迭代器进行遍历
- 5.通过Java8 Lambda表达式遍历
HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的
hash算法hash算法又叫摘要算法(Digest),是将给定数据转化为固定长度的不规则值的函数
- 输出值的数据长度固定(同一hash函数下)
- 相同的输入必定得到相同的输出值
- 相似的数据得到的输出值的差距也许会有点大
- 输入不同的数据有可能得到一致的输出值(哈希碰撞,也叫哈希冲突)
在java中,hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数
//Object类的hashCode方法,返回的是对象的32位jvm内存地址 public native int hashCode();
使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用
String类重写了hashCode()方法
public int hashCode() {
int h = hash; //Default to 0
if (h == 0 && value.length > 0) { //private final char value[];
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
既然是hash算法,那么碰撞就不可能完全避免的
因为碰撞的概率关系到了算法的安全性,所以一个安全的哈希算法必须满足
- 碰撞概率低
- 不能通过哈希值算出原值
| 算法 | 输出长度(字节) | 输出长度(位) |
|---|---|---|
| MD5 | 16 bytes | 128 bits |
| SHA-1 | 20 bytes | 160 bits |
| RipeMD-160 | 20 bytes | 160 bits |
| SHA-256 | 32 bytes | 256 bits |
| SHA-512 | 64 bytes | 512 bits |
根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全
hashmap的数据结构jdk1.8之前:数组+链表
jdk1.8之后:数组+链表+红黑树
jdk1.7和jdk1.8的区别 简单讲解过程大致过程大家应该都知道,简单讲就是hashcode结合数组长度,得到数组索引,空数组则放入,有元素则比对hash值,hash值相同则发生哈希碰撞,用equals比对key是否一致,一致则取到对应的值(put则覆盖),不一致就继续比对,到尾结点了就补链,链长度过长影响效率,默认链表长度大于为8且数组长度大于64时转红黑树
1.8之后为什么要引入红黑树?哈希算法再好也无法避免哈希碰撞,当碰撞过多,接入链表时,前期数据少,效率还行
但是后期的不断碰撞,导致链表过长,读取需要遍历,时间消耗O(n)
而引入红黑树后,查找时间O(logn),提高了效率
什么时候扩容?
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//扩容条件1:table为null 或 tab长度为0
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//扩容条件2:size是否大于了边界值
if (++size > threshold)
resize();
第一个条件没啥好说的,空的就扩呗
第二个条件就需要多注意一下了
-
size表示hashmap中k-v的实际数量,并不是数组的长度
-
边界值threshold = 容量DEFAULT_INITIAL_CAPACITY * 负载因子DEFAULT_LOAD_FACTOR
所以达到扩容的阈值点就是16*0.75 = 12
hashmap扩容是扩容一倍newThr = oldThr << 1; // double threshold(二进制左移一位就是数字翻倍)
cap容量给了老边界值newCap = oldThr;
如果老边界值为0则给初始值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
高频集合扩容的信息
| 集合类 | 初始容量 | 负载因子 | 扩容增量 |
|---|---|---|---|
| ArrayList | 10 | 1 | 0.5倍 |
| Vector | 10 | 1 | 1倍 |
| HashSet | 16 | 0.75 | 1 倍 |
| HashMap | 16 | 0.75 | 1 倍 |
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;
链表长度阈值8,数组长度大小64
链表阈值至少大于2且至少为8,才能符合红黑树在收缩的时候能转为普通链表
数组长度至少为4倍的链表阈值以避免冲突
if (binCount >= TREEIFY_THRESHOLD - 1) //先判断是否达到链表阈值
treeifyBin(tab, hash);
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//再判断是否达到数组阈值
resize();
HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?
答:对于 key 的 hashCode 做 hash 操作,无符号右移 16 位然后做异或运算。还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移 16 位异或运算效率是最高的。
当两个对象的 hashCode 相等时会怎么样?答:会产生哈希碰撞。若 key 值内容相同则替换旧的 value,不然连接到链表后面,链表长度超过阈值 8 就转换为红黑树存储。
什么是哈希碰撞,如何解决哈希碰撞?答:只要两个元素的 key 计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。
如果两个键的 hashCode 相同,如何存储键值对?答:通过 equals 比较内容是否相同。相同:则新的 value 覆盖之前的 value。不相同:则将新的键值对添加到哈希表中。
HashMap集合类的成员public class HashMapextends AbstractMap implements Map , Cloneable, Serializable
Cloneable表示可克隆,Serializable可序列化
这两个接口都是一个声明接口
public interface Serializable {
}
public interface Cloneable {
}
hashmap继承了AbstractMap
private static final long serialVersionUID = 362498820763181265L;
2.集合的初始容量序列化的含义、意义及使用场景
- 序列化:将对象写入到IO流中
- 反序列化:从IO流中恢复对象
- 意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
默认初始容量 - 必须是 2 的幂,它为什么必须是2的幂呢?
大家知道hashmap为了存取高效,要尽量减少哈希碰撞,即:让数据分配均匀,数组空间都占据,链表长度接近
而hashmap的算法就是取模hash%length,计算机直接取余的效率不如位运算
所以源码中做了优化hash&(length-1),而hash%length = hash&(length-1)前提就是length是2的幂次
&:按位与运算,两个当且仅当都为1的时候结果才为1
扩容是非常消耗性能的,默认的容量不一定适用,所以需要创建的就指定一个适合的大小才是正确的
那我自定义给的容量不是2的N次方呢?会给一个比自定义容量更大一些的2n的数,例如14时就给16,20时就给32
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;
}
3.负载因子(加载因子)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
上文已经讲得很清楚了,这里不再赘述
4.集合最大容量static final int MAXIMUM_CAPACITY = 1 << 30;
最大容量是230,左移30位
5.树化阈值(转红黑树的边界值)static final int TREEIFY_THRESHOLD = 8; static final int MIN_TREEIFY_CAPACITY = 64;
当链表长度大于8时会转为红黑树
(其实也需要数组长度大于64,上文也提到了)
final void treeifyBin(Node[] tab, int hash) { int n, index; Node e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); ...... }
源码中的讲解是:到8转为红黑树 到6转为链表
6.取消数化阈值(树转链表)红黑树的占用空间比链表要大很多,所以一开始还是链表会更好 ->空间和时间的权衡
为什么转化为红黑树的阈值8和转化为链表的阈值6不一样,是为了避免频繁来回转化
红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3, 而log(6)=2. 6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
static final int UNTREEIFY_THRESHOLD = 6;
到8转为红黑树 到6转为链表
The value must be greater than 2 and should be at least 8 to mesh with assumptions in tree removal about conversion back to plain bins upon shrinkage.
该值必须大于 2 并且应该至少为 8 以符合树移除中关于在收缩时转换回普通 bin 的假设。
我们可以看见收缩转回普通bin,证明hashmap会从红黑树收缩为链表
HashMap的构造方法 1.默认And when they become too small (due to removal or resizing) they are converted back to plain bins.
当它们变得太小时(由于移除或调整大小),它们会被转换回桶。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
用的最多的,值都是默认值,也许并不是最好的选择
2.指定容量public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
因为扩容是非常消耗性能的,所以我们可以自定义容量
可以看到其实指定容量,就是把自定义的容量和默认的负载因子传给的下面的方法↓
3.指定容量和负载因子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);
}
this.threshold = tableSizeFor(initialCapacity);
指定的容量必须为2n,上文也提到了那我自定义给的容量不是2的N次方呢?的问题,这里不再赘述
4.指定集合构造器 public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
为什么float ft = ((float)s / loadFactor) + 1.0F;里要加这个1.0F呢?
一句话:为了得到更大的容量来减少扩容,提高效率
HashMap的成员方法 1.增加puts/loadFactor的结果是小数,加1.0F与(int)ft相当于是对小数做一 个向上取整以尽可能的保证更大容量,更大的容量能够减少resize的调用次数
所以+1.0F是为了获取更大的容量
例如:原来集合的元素个数是6个,那么6/0.75是8, 是2的n次幂,那么新的数组大小就是8了。然后原来数组的数据就会存储到长度是8的新的数组中了,这样会导致在存储元素的时候,容量不够,还得继续扩容,那么性能降低了
而如果+1呢,数组长度直接变为16了,这样可以减少数组的扩容
大概步骤如下
- 先通过hash值计算出key映射到哪个桶(数组索引)
- 比对hash,如果桶上没有碰撞冲突,则直接插入
- 如果出现碰撞冲突了,则需要处理冲突
- 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据
- 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树
- 比对key,如果桶中存在重复的键,则为该键替换新值value
- 如果size大于阈值threshold,则进行扩容
//HashMap
public V put(K key, V value) {
//计算key的hash值传给puval方法
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
//可以看到,key为null时,hash值为0
//key的hashCode()无符号右移16位 再与 原值 异或^ -> 得到hash值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//之前提到了hash和数组长度进行 按位与,得到索引
//(tab.length-1)&hash的结果与hash%tab.length结果一致(length是2的幂次方情况下)
n = tab.length;
tab[i = (n - 1) & hash]
有点意思的是,hashmap此处key为null时返回索引为0
而HashTable中并没有判空操作,所以hashtable的key不能为null
//HashTable put()
if (value == null) {
throw new NullPointerException();
}
int hash = key.hashCode();
2.核心增加putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)
数组table的创建
hashmap的数组是随着new HashMap就被创建了嘛?
并不是,而是随着第一次putVal进行了判空而创建的(调用了扩容方法)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
数组的索引的算法
n = tab.length tab[i = (n - 1) & hash]
上文提到了,这里再复制一遍
大家知道hashmap为了存取高效,要尽量减少哈希碰撞,即:让数据分配均匀,数组空间都占据,链表长度接近
而hashmap的算法就是取模hash%length,计算机直接取余的效率不如位运算
所以源码中做了优化hash&(length-1),而hash%length = hash&(length-1)前提就是length是2的幂次
3.转化红黑树treeifyBin除非表太小,否则替换给定哈希索引处bin中的所有链接节点,在这种情况下改为调整大小
//putVal方法中,循环链表,判断count>=链表阈值-1时,进行转化
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;
}
在讲解转化方法之前,先讲一讲什么是2-3-4树和红黑树
这里推荐一个数据结构学习网站
什么是2-3-4树?2-3-4树是四阶的B树(Balance Tree),他属于一种多路查找树,它的结构有以下限制:
所有叶子节点都拥有相同的深度
节点只能是2-节点、3-节点、4-节点
- 2-节点:包含1个元素的节点,有2个子节点
- 3-节点:包含2个元素的节点,有3个子节点
- 4-节点:包含3个元素的节点,有4个子节点
所有节点必须至少包含1个元素
元素始终保持排序顺序,整体上保持二叉查找树的性质,即父结点大于左子结点,小于右子结点
而且结点有多个元素时,每个元素必须大于它左边的和它的左子树中元素
2-3-4树的查询操作像普通的二叉搜索树一-样,非常简单,但由于其结点元素数不确定,在一些编程语言中实现起来并不方便, 实现一般使用它的等同——红黑树
2-3-4树和红黑树的关系- 2节点 - 黑节点
- 3节点 - 左倾/右倾
- 4节点- 中间上移
红黑树Red-Black-Tree [RBT] ,是一个自平衡的二叉查找树,每个节点都遵守下面的原则
-
每个节点要么是黑色,要么是红色
-
根节点是黑色
-
每个叶子节点是黑色
-
一棵树当中没有子结点(即度为0)的结点称为叶子结点
-
-
每个红色节点的两个子节点一定都是黑色
- 图中红色节点没有子节点是因为默认隐藏掉了为null的黑色节点
-
任意一个节点到每个叶子节点的路径都包含数量相等的黑节点
红黑树能自平衡靠的是什么?左旋、右旋、变色
左旋:子左给父右,父子交换
以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,
右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
右旋:子右给父左,父子交换
以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,
左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
变色
红黑树对于插入节点的分析节点颜色由黑边红或者由红变黑
对于一颗普通的二叉搜索树来说,就是左小右大
红黑树的插入也是这样的,只是插入完可能是需要做平衡处理(旋转、变色)
红黑树平衡的调整处理
- 2-3-4树2节点新增一个元素直接合并为3节点
- 红黑树:新增一个红色节点这个红色节点会添加在黑色节点下:不需要调整
- 2-3-4树3节点新增个元素 合并为一个4节点
- 红黑树:就会有6种情况两种(左中右)的不需要调整
- 根左左 根右右 旋转1次
- 根左右 根右左 旋转2次
- 2-3-4树4节点新增一个元素4节点中间元素升级为父节点新增元素和剩下节点合并
- 红黑树:新增节点是红色+爷爷节点是黑色父 节点和叔叔节点为红色
- 调整为爷爷节点变为红色,父亲和叔叔节点变为黑色,如果爷爷节点为root节点则调整为黑色
我们先看一颗普通的二叉树
前驱节点:对一棵二叉树进行中序遍历,遍历后的顺序,当前节点的前一个节点为该节点的前驱节点
后继节点:对一棵二叉树进行中序遍历,遍历后的顺序,当前节点的后一个节点为该节点的后继节点
例如一颗完全二叉树(1,2,3,4,5,6,7),按照中序遍历后的顺序为:(4,2,5,1,6,3,7)
即:1节点的前驱节点为:5,后继节点为:6
而删除操作就是有3种情况
- 删除叶子节点,直接删除
- 删除的节点有一个子节点,那么用子节点来替代
- 如果删除的节点右两个子节点,此时需要找到前驱节点或者后继节点来替代可以转换为1、2的情况
//传入数组tab和hash值用于确定下标 final void treeifyBin(Node[] tab, int hash) { //n是数组长度,index是下标,e是根据hash值从数组中取的节点 int n, index; Node e; //判断数组还有没有容量及是否大于64的数组阈值,没则扩容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); //桶中节点不为空则循环转换红黑树节点 else if ((e = tab[index = (n - 1) & hash]) != null) { //hd链表头结点,tl尾结点 TreeNode hd = null, tl = null; do { //将普通节点转为树节点 TreeNode p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); //链表处理变treeNode后放置回桶中 if ((tab[index] = hd) != null) //将红黑树节点链表进行平衡操作,才是最终变为红黑树 hd.treeify(tab); } }
将节点转为树节点
TreeNodereplacementTreeNode(Node p, Node next) { return new TreeNode<>(p.hash, p.key, p.value, next); }
创建红黑树
final void treeify(Node4.扩容resize[] tab) { TreeNode root = null; // 定义树的根节点 for (TreeNode x = this, next; x != null; x = next) { // 遍历链表,x指向当前节点、next指向下一个节点 next = (TreeNode )x.next; // 下一个节点 x.left = x.right = null; // 设置当前节点的左右节点为空 if (root == null) { // 如果还没有根节点 x.parent = null; // 当前节点的父节点设为空 x.red = false; // 当前节点的红色属性设为false(把当前节点设为黑色) root = x; // 根节点指向到当前节点 } else { // 如果已经存在根节点了 K k = x.key; // 取得当前链表节点的key int h = x.hash; // 取得当前链表节点的hash值 Class> kc = null; // 定义key所属的Class for (TreeNode p = root;;) { // 从根节点开始遍历,此遍历没有设置边界,只能从内部跳出 // GOTO1 int dir, ph; // dir 标识方向(左右)、ph标识当前树节点的hash值 K pk = p.key; // 当前树节点的key if ((ph = p.hash) > h) // 如果当前树节点hash值 大于 当前链表节点的hash值 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); 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; } } } } // 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的 // 因为我们要基于树来做查找,所以就应该把 tab[N] 得到的对象一定根是节点对象,而目前只是链表的第一个节点对象,所以要做相应的处理。 //把红黑树的根节点设为 其所在的数组槽 的第一个元素 //首先明确:TreeNode既是一个红黑树结构,也是一个双链表结构 //这个方法里做的事情,就是保证树的根节点一定也要成为链表的首节点 moveRootToFront(tab, root); }
扩容机制在上文讲述了,这里不再赘述
容量翻倍final Node重新计算,并旧转新[] resize() { //获取扩容之前的元素列表 Node [] oldTab = table; //获取之前的列表长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //如果原来的容量已经大于了最大容量,则把threshold设置成int的最大值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //否则,新的容量,扩充为原来的一杯oldCap << 1 向左位移1为,相当于oldCap * 2 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //如果走下面这个else,则说明是第一次put元素,也是第一次进行扩展 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; //扩容 = 扩容因子 * 容量,DEFAULT_INITIAL_CAPACITY 默认是16,DEFAULT_LOAD_FACTOR默认是0.75 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
//到此为止,如果是第一次扩容的时候,也就是原来没有元素,下面的代码不会运行
//如果原来有元素,则将原来的元素,进行放到新扩容的数组里面
if (oldTab != null) {
//循环原来的列表,将旧数组放置新数组中
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果列表里面只有一个元素,直接将e的key的hash与新容量重新计算下标
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树,则进行红黑树的复制
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else {
//低位位链表头和尾
Node loHead = null, loTail = null;
//高位链表头和尾
Node hiHead = null, hiTail = null;
Node next;
do {
//获取当前节点的下一个元素
next = e.next;
if ((e.hash & oldCap) == 0) {
//如果loTail==null说明是第一个元素,把这个元素赋值给loHead
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;
//下标 j + oldCap 计算出来的新下标正好是原来位置索引加上新的容量
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
扩容方法中JDK8对JDK7的优化
在jdk7中,所有元素按照索引算法全部重新计算,效率比较低
可以看到在1.7中,借助transfer()方法(jdk1.8中已移除),在扩容的时候会重新计算threshold,数组长度并进行rehash,这样的效率是偏低的
//jdk1.7
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()方法把原数组中的值放到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//设置hashmap扩容后为新的数组引用
table = newTable;
//设置hashmap扩容新的阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {
while(null != e) {
Entry next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//通过key值的hash值和新数组的大小算出在当前数组中的存放位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
在jdk8中,在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就行
e.hash & oldCap进行位运算,判断结果
- 是0的话,索引没变:原位置 newTab[j] = loHead;
- 是1的话,索引变成:原索引+oldCap(原位置+旧容量) newTab[j + oldCap] = hiHead;
正是因为这样巧妙的rehash方式,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认
为是随机的,在resize的过程中保证了rehash之后每个桶上的节点数一定小于等于原来桶上的节点数,保证了
rehash之后不会出现更严重的hash冲突,均匀的把之前的冲突的节点分散到新的桶中了。
//jdk1.8
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;
}
//扩容后的位置=原位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//扩容后的位置 = 原位置 + 旧容量
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
从上面的源码中可以分析出扩容分为三种情况:
5.删除remove第一种是初始化阶段,此时的newCap和newThr均设为0,从之前的源码可知,第一次扩容的时候,默认的阈值就是threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。DEFAULT_INITIAL_CAPACITY为16, DEFAULT_LOAD_FACTOR 为0.75.
第二种是如果oldCap大于0,也就是扩容过的话,每次table的容量和threshold都会扩围原来的两倍
第三种是如果指定了threshold的初始容量的话,newCap就会等于这个threshold,新的threshold会重新计算
和put一样,为用户提供一个方法,实际上调用的removeNode
传递key的hash值选中位置,传递key以选中具体元素进行删除
判断删除的节点存不存在,存在则删除并返回节点的值value,反之返回null
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
6.核心删除removeNode
final Node7.获取getremoveNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node [] tab; Node p; int n, index; // 声明节点数组、当前节点、数组长度、索引值 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node node = null, e; K k; V v; // 定义要返回的节点对象,声明一个临时节点变量、键变量、值变量 // 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { // 如果当前节点是TreeNode类型,说明已经是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点 if (p instanceof TreeNode) node = ((TreeNode )p).getTreeNode(hash, key); // 如果不是树节点,那么就是一个链表,只需要从头到尾逐个节点比对即可 else { do { // 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量,调出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } // 走到这里,说明e也没有匹配上 p = e; // 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点 } while ((e = e.next) != null); // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点 } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) // 如果该节点是个TreeNode对象,说明此节点存在于红黑树结构中,调用removeTreeNode方法(该方法单独解析)移除该节点 ((TreeNode )node).removeTreeNode(this, tab, movable); else if (node == p) // 如果该节点不是TreeNode对象,node == p 的意思是该node节点就是首节点 tab[index] = node.next; // 由于删除的是首节点,那么直接将节点数组对应位置指向到第二个节点即可 else // 如果node节点不是首节点,此时p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了 p.next = node.next; ++modCount; // HashMap的修改次数递增 --size; // HashMap的元素个数递减 afterNodeRemoval(node); // 调用afterNodeRemoval方法,该方法HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆写 return node; } } return null; }
和put与remove一样,提供一个对外的方法,实际上是调用的getNode方法
如果有值返回值,无值返回null
将key的hash值与key传给了getNode
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
8.核心获取getNode
先用hash位运算得到下标first = tab[(n - 1) & hash]
判断桶的第一个元素是不是想要的(k = first.key) == key或者key != null && key.equals(k)
也就是比对key是否一致,一致则返回
否则就先判断是不是红黑树,是则用getTreeNode获取并返回
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
不是红黑树,则循环链表并和之前一样比对key并判断
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
源码如下
final Node遍历hashmap的5种方式 1.循环遍历map自带的entrySet中的entrygetNode(int hash, Object key) { Node [] tab; Node first, e; int n; K k; 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; }
map的entrySet()方法可以返回Set
循环遍历set可以得到Map.Entry
而Map.Entry
Map2.ForEach迭代键值对方式map = new HashMap (); map.put(1, 10); map.put(2, 20); Set > entries = map.entrySet(); for (Map.Entry entry : entries) { System.out.println("key ="+entry.getKey()+" value ="+entry.getValue()); }
map.keySet()和map.values()包含了所有的key和value,直接遍历即可
Map3.带泛型的map的entrySet的迭代器进行遍历map = new HashMap (); map.put(1, 10); map.put(2, 20); // 迭代键 for (Integer key : map.keySet()) { System.out.println("Key = " + key); } // 迭代值 for (Integer value : map.values()) { System.out.println("Value = " + value); }
map.entrySet()返回的set可以使用iterator()方法得到迭代器
entries.next()得到entry,和第一个方法一样getKey和getValue就行
Map4.不带泛型的map的entrySet的迭代器进行遍历map = new HashMap (); map.put(1, 10); map.put(2, 20); Iterator > entries = map.entrySet().iterator(); while (entries.hasNext()) { Map.Entry entry = entries.next(); System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); }
和第3种方法差不多,区别只有不带泛型,取值需要强转
Map map = new HashMap();
map.put(1, 10);
map.put(2, 20);
Iterator entries = map.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry entry = (Map.Entry) entries.next();
Integer key = (Integer) entry.getKey();
Integer value = (Integer) entry.getValue();
System.out.println("Key = " + key + ", Value = " + value);
}
5.通过Java8 Lambda表达式遍历
Mapmap = new HashMap (); map.put(1, 10); map.put(2, 20); map.forEach((k, v) -> System.out.println("key: " + k + " value:" + v));



