概述
前言
jdk 版本 jdk1.8.0_161
结构图
说明:
Map:映射 键 和 值 的 对象。 map 不能 包含 重复的键,每个键 至多 只能 映射 一个 值。
map 提供 三种 集合视图: 分别是 键(key) 的 集合 (Set);值(value)的 集合(Set);键值对(key-value)映射 的 集合(Set);
AbstractMap:Map 的 骨架实现。
HashMap:Map 接口 基于 哈希表(hash table)的实现,支持 null 值 和 null 键,非同步的,不保证 map 的插入顺序。 源码底层数据结构包括 数组 + 链接列表 + 红黑树。hash table相关知识 参看 维基百科 和 相关翻译。底层结构图类似下面的结构:
HashMap 类 大致上 等同于 HashTable,区别在于: HashMap 是 非同步的,而且支持 null;而 HashTable 是 线程安全的 ,不支持 null。HashMap 的实例 有两个参数影响其性能:初始化容量、加载因子。
容量是指 哈希表中 桶的数量,而初始容量是 在哈希表被创建的时候 的容量。默认容量为 16.
加载因子 是指 在容量自动增加之前 哈希表允许能够填充多满的 量度。默认加载因子为 0.75.
当哈希表中的 条目(entries) 数量超过 加载因子 和 当前容量的 乘积时,哈希表 进行散列操作(rehash)(也就是内部数据结构被重新构建 ),使得 哈希表拥有 两倍于之前 桶的数量。
一般来说,默认的加载因子(0.75) 在 时间花费 和 空间花费 上 达到了很好的平衡。更高的值虽然减少了 空间开销,但是增加了查找花费时间。在设置初始容量时,应考虑map中预期的条目数目及其负载因子,以此来最小化 散列操作的次数。如果初始容量大于 条目最大数 除以 加载因子的值,则不会发生任何重散列操作。
如果 HashMap 中需要 保存 很多映射的键值对,创建一个大容量的 HashMap 比 让它自己 根据需要自动重整 效率更高。注意,多个 key 使用 同一个 hashcode() 方法 会 降低 哈希表的性能
源码
构造函数
主要是根据 初始化容量 和 加载因子 构建 :初始化容量决定了 threshold 的大小:大于等于 初始化容量的 最小 2 的次幂,如果未指定 初始化容量,则 threshold 为 0. 比如初始化容量为 3 对应的 threshold 为 4, 初始化容量为9 对应的 threshold 为 16;
/**
* 使用指定的初始化容量 和 加载因子
* 构建一个空的 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);
}
/**
* 下一次扩容临界数值(加载因子 * 容量),也是扩容后的 size 值
*/
int threshold;
/**
* 返回大于等于给定容量的最小 2 次幂值
*/
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;
}
这里主要讲解一下 tableSizeFor() 方法:
该方法的目的是 求出 大于等于 给定数值的最小2次幂:
已知 2^0 + 2^1 + 2^2 + ... + 2^n = 2 ^(n+1) - 1 ;(等比数列求和公式)
而 二进制 转换为 十进制公式 为 a0*2^0 + a1*2^1 + an*2^n (a0、a1、an 依次对应从右到左的二进制数);
例如:9(10进制) = 1001(2进制) ;计算过程为 :
1*2^0 + 0*2^1 + 0*2^2 + 1*2^3 = 9 < 2^0 + 2^1 + 2^2 + 2^3 = 2^4 - 1 :大于9 的 最小2次幂为 2^4 = 16;
所以 求 大于 给定数值的最小2次幂 只要 将该值对应的二进制中 最高位数为 1的 低位都转为1 的二进制对应的 十进制 + 1 即可;
这里的代码逻辑是 如果给定的数值就是 2次幂的话,就用本身的数值,而不再向上求取;所以有:
int n = cap - 1;
例如 对于 cap = 9,此时 n = 8;对应二进制: 0000 0000 0000 0000 0000 0000 0000 1001
自身值 与 自身值无符号右移1位 的 或操作:将为 1 的 最高位 传播到 次最高位,这个时候最高位和次高位 都为 1
n |= n >>> 1; // 或操作:任一操作数为 1 则返回1,否则返回 0
0000 0000 0000 0000 0000 0000 0000 1001 | 0000 0000 0000 0000 0000 0000 0000 0100
= 0000 0000 0000 0000 0000 0000 0000 1101
同理,做无符号右移2位:这里移动两位是因为前一个操作已经生成了2个 1,这个操作将会复制这两个1成为4个1。
n |= n >>> 2;
0000 0000 0000 0000 0000 0000 0000 1101 | 0000 0000 0000 0000 0000 0000 0000 0011
= 0000 0000 0000 0000 0000 0000 0000 1111
依次 无符号右移 4位,8位,16位:因为 Integer 的 最大值为 2^31-1,所以只处理到 无符号右移16位(32个1)。
对于9 后面的运算都相当于没操作,所以 最后的 二进制为
0000 0000 0000 0000 0000 0000 0000 1111 = 2^4 - 1 , 所以最小2次幂为 2^4 = 16。
内部数据结构
数组:桶数组,保存 Node
/**
* 在第一次使用的时候初始化,在需要的时候调整大小。
* 一旦分配,数组长度永远是 2 的 次幂
* (在某些操作中也允许 长度为 0)
*/
transient Node<K,V>[] table;
Node 内部类:哈希表中的桶,保存了值(包括键、键对应的 hash 、值)、下一个Node节点;类似于一种单向链表.实现了 Map.Entry,本质上是 键 和 值 的 映射。
( LinkedList 中的 Node 节点:记录了值,上一个Node 节点 和 下一个 Node 节点的 双向链表)
/**
* 基础的 hash 桶 节点,被用于 大多数的 条目(entry)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
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;
}
// some methods
}
TreeNode 内部类:HashMap.Node 的 间接子类,红黑树数据结构,详情查看wiki
/**
*
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 红黑树 链接
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; //删除时需要断开链接的节点
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// some methods
}
增删改查方法
增加元素:put(key , value)
这里涉及到两个参数的扩容机制:
临界值 threshold 的增加:构造函数中初始化 threshold 值 为 大于等于 给定容量的最小 2次幂,或者等于0(未指定初始化容量时)。在这之后的增加规则为:如果threshold 等于 0 则 使用默认的初始化容量 乘以加载因子得出 新的 threshold;如果threshold 小于 默认的初始化容量 16 且 大于 0 ,则使用 threshold 乘以 加载因子得出 新的 threshold,如果 threshold 大于 默认的初始化容量 16,则变化 原先的 2倍。
桶数组 table 的扩容:如果 table 未初始化,则第一次table 初始化的容量值为 threshold 的值(构造函数中初始化了 threshold 值);如果 table 已经初始化,则 扩容的值为 原先的 2倍;
元素保存机制:
通过 给定的 key 计算 hash 然后与 桶数组的长度 减一 按位与 计算出 该 key 在 桶数组中的 索引,索引对应的值 为 链表结构,如果对应值为空,直接构造一个节点赋值给该索引;如果对应值不为空,且链表长度小于 8,则校验该 key 是否属于 重复的 key,是则替换原先的值,否则插入到链表的尾部;如果链表长度等于8,且插入的键不重复,则把链表树化成红黑树;如果链表长度大于8 则按照 红黑树逻辑结构处理(详细略)。
/**
* 在 map 中将 指定的 键 和 指定的值关联
* 如果 map 已经存在了这个键值,则
* 对应的值会被替换为新的值
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 计算 hash
* 由于 jdk1.8中采用了红黑树来处理过多的碰撞,相对提高了效率
* 所以这里只是简单的 异或 高位转低位的值来 减少 碰撞,
* 这种异或虽然简单但是保留了高位 和低位的特征,减少了碰撞
* jdK1.7 之前异或四次之多
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* Implements Map.put and related methods
*
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; // p 指需要插入的key value 对应的节点数据
if ((tab = table) == null || (n = tab.length) == 0) //桶数组未初始化
n = (tab = resize()).length; //初始化后 桶数组
if ((p = tab[i = (n - 1) & hash]) == null) // (n-1)&hash 求出该值在 桶数组中的索引 的 便捷方法;桶中是否已存在对应链表
tab[i] = newNode(hash, key, value, null); // 不存在对应链表,直接将节点 插入数组的索引处
else { // 存在对应链表
Node<K,V> e; K k; // e 指 已经存在插入 键对应的 节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // 判断链表头节点是否和要插入的 key 相同
e = p; //是相同的 ,则使用原来的 链表,稍后修改键 值即可
else if (p instanceof TreeNode) // 红黑树 的碰撞处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //既不是相同键也不是红黑树 的碰撞处理,即table索引处的链表长度小于9
for (int binCount = 0; ; ++binCount) { // 遍历 桶数组对应索引处的 所有链表
if ((e = p.next) == null) { //如果是单向链表的最后一个节点
p.next = newNode(hash, key, value, null); //将新添加的节点链接到当前最后一个几点上
if (binCount >= TREEIFY_THRESHOLD - 1) //当链表长度超过8时
treeifyBin(tab, hash); // 桶 的 树化
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) // 判断 链表上的每一个链表是否 和 要插入的key 相同
break; //相同
p = e; // 以上两个条件都不满足,判断下一个
}
}
if (e != null) { // 插入的键 对应的节点已存在,处理对应的值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; //将原来的值 替换为 插入的值
afterNodeAccess(e);// LinkedHashMap 需要的操作
return oldValue;
}
}
// 不存在 对应键的映射 的后续处理:是否扩容
++modCount;
if (++size > threshold) // size 为 map 中已映射的键值对的数量(哈希碰撞的不包括在内)
resize();
afterNodeInsertion(evict); // LinkedHashMap 需要的操作
return null;
}
/**
* 在第一次使用的时候初始化,必要的时候重新分配大小
* 数组的长度始终是 2的次幂
*/
transient Node<K,V>[] table;
/**
* 初始化 table 或者 将 table 的长度加倍
*
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;// 保存节点数组的原始容量
int oldThr = threshold;//容器扩容的临界值
int newCap, newThr = 0;
if (oldCap > 0) { // table 已初始化,直接扩容为2倍
if (oldCap >= MAXIMUM_CAPACITY) { // 超出最大容量限制
threshold = Integer.MAX_VALUE;//下一次扩容容量为最大值
return oldTab;//容量已经最大,不再扩容
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) //扩容后容量 介于最大值 和最小值之间
newThr = oldThr << 1; // 新的 threshold计算方法:变为 原来的 2倍
}
else if (oldThr > 0) // 扩容的临界值已初始化
newCap = oldThr;// 直接赋值为新的容量
else { // table 未初始化 且 threshold 未初始化,使用默认值
newCap = DEFAULT_INITIAL_CAPACITY; //默认容量为 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的 threshold 为加载因子 和 容量的乘积
}
if (newThr == 0) { //桶数组的长度 小于 默认初始化容量 16,新threshold 计算方法
float ft = (float)newCap * loadFactor;// 容量 乘以 指定的加载因子
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//初始化新的节点数组
table = newTab;
// 如果原来的table有数据,则将数据复制到新的table中
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 = 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;
}
// 创建 Node 类
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
// 是否更换为红黑树 的 临界值
static final int TREEIFY_THRESHOLD = 8;
/**
* 树化桶 :通过给定的 hash替换数组索引处的桶的所有链接节点
* 如果 桶数组 太小的话,则将重新调整大小
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)// 桶数组未空 或者 容量 小于 64
resize(); // 重新调整大小
else if ((e = tab[index = (n - 1) & hash]) != null) { // 桶数组对应索引处的链接节点是否为空
TreeNode<K,V> hd = null, tl = null; // hd 保存 头节点 tl 保存前一个节点
do {
TreeNode<K,V> p = replacementTreeNode(e, null); // 将node 构造成 treeNode
if (tl == null) // 前一个节点为空,即 p 为 头节点
hd = p;
else { //p 为非头节点
p.prev = tl;
tl.next = p; //前一个节点的下一个节点即为当前节点
}
tl = p; // 当前节点置位 前一个节点,继续循环
} while ((e = e.next) != null); // 循环直到最后一个节点
if ((tab[index] = hd) != null)
hd.treeify(tab); // 根节点特殊处理
}
}
/**
* 对获取的元素排序,LinkedList accessOrder 为 false,此处不处理
*/
void afterNodeAccess(Node<K,V> e) { // 将节点移动到尾部
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
/**
* 不做 任何操作
*/
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
// 永远返回 false
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
删除元素:remove(key)
删除机制:
通过给定键计算 hash 然后 与 桶数组长度减一 按位与 操作 计算出 对应桶数组的 索引。通过索引得出 链表(或者红黑树)的头节点,如果该节点与要删除的键 吻合,则确定删除的就是这个头节点,否则 循环遍历 该 链表(或者红黑树),直到找到吻合的 节点对象,最后删除找到的节点。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
修改:可以通过 key 先 进行查询 ,然后 再 put 插入 操作进行覆盖。
查询:
删除逻辑中 包含了 查询的逻辑。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> 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<K,V>)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;
}
模拟 hash 碰撞:
即自定义 hashCode 方法 和 equals 方法,现有如下实体 bean Student 类:
student 的 name 相同则 hashCode 相同,最后运算处的 table 索引相同,计算方式为:
(hash=(h = key.hashCode()) ^ (h >>> 16))&(table.length - 1)
package com.ycit.bean;
import java.time.LocalDate;
import java.util.Objects;
/**
* 学生类
*
* @author xlch
* @create 2017-05-17 22:15
*/
public class Student {
private String name;
private LocalDate birthday;
public Student() {
}
public Student(String name, LocalDate birthday) {
this.name = name;
this.birthday = birthday;
}
// getter setter
@Override
public boolean equals(Object o) {
System.out.println("equals invoke, equals obj: " + super.equals(o));
return super.equals(o);
}
@Override
public int hashCode() {
System.out.println(name + " hashcode is " + this.name.hashCode());
return this.name.hashCode();
}
}
测试代码:
@Test
public void collisionTest() {
Student student = new Student("小明", LocalDate.of(2018, 4, 5));
Student student2 = new Student("小明", LocalDate.of(2018, 4, 7));
HashMap<Student, String> map = new HashMap<>();
map.put(student, "2");
map.put(student2, "1"); //此时插入的数据 和 student 发生 碰撞
}
输出:
小明 hashcode is 756703
小明 hashcode is 756703
equals invoke, equals obj: false
最后
以上就是机灵皮卡丘为你收集整理的【collection】集合学习——Map 之 HashMap的全部内容,希望文章能够帮你解决【collection】集合学习——Map 之 HashMap所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复