《转》HashMap相关内容整理

HashMap 概述

HashMap 是 Map 接口的实现,HashMap 允许空的 key-value 键值对,HashMap 是一个非线程安全的容器。HashMap 是无序的。
HashMap 的底层数据结构是数组 + 链表的集合体,数组在 HashMap 中又被称为桶(bucket)。遍历 HashMap 需要的时间损耗为 HashMap 实例桶的数量 + (key - value 映射) 的数量。
HashMap 实例有两个很重要的因素,初始容量和负载因子,初始容量指的就是 hash 表桶的数量,负载因子是一种衡量哈希表填充程度的标准,当哈希表中存在足够数量的 entry,以至于超过了负载因子和当前容量,这个哈希表会进行 rehash 操作,内部的数据结构重新 rebuilt。

HashMap 和 HashTable 的区别

. 相同点
HashMap 和 HashTable 都是基于哈希表实现的,其内部每个元素都是 key-value 键值对,HashMap 和 HashTable 都实现了 Map、Cloneable、Serializable 接口。

不同点
1、父类不同:HashMap 继承了 AbstractMap 类,而 HashTable 继承了 Dictionary 类
2、空值不同:HashMap 允许空的 key 和 value 值,HashTable 不允许空的 key 和 value 值。HashMap 会把 Null key 当做普通的 key 对待。不允许 null key 重复。
3、线程安全性:HashMap 不是线程安全的,而 HashTable 本身就是线程安全的容器。
4、性能方面:虽然 HashMap 和 HashTable 都是基于单链表的,但是 HashMap 进行 put 或者 get􏱤 操作,可以达到常数时间的性能;而 HashTable 的 put 和 get 操作都是加了 synchronized 锁的,所以效率很差。
5、初始容量不同:HashTable 的初始长度是11,之后每次扩充容量变为之前的 2n+1(n为上一次的长度)。而 HashMap 的初始长度为16,之后每次扩充变为原来的两倍。创建时,如果给定了容量初始值,那么HashTable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。

HashMap 和 HashSet 的区别

HashSet 继承于 AbstractSet 接口,实现了 Set、Cloneable,、java.io.Serializable 接口。HashSet 不允许集合中出现重复的值。HashSet 底层其实就是 HashMap,所有对 HashSet 的操作其实就是对 HashMap 的操作。所以 HashSet 也不保证集合的顺序。

HashMap 底层结构

在这里插入图片描述

最主要的三个类(接口)就是 HashMap,AbstractMap和 Map 了,HashMap 我们上面已经在概述中简单介绍了一下,下面来介绍一下 AbstractMap。

AbstractMap 类

这个抽象类是 Map 接口的骨干实现,以求最大化的减少实现类的工作量。为了实现不可修改的 map,程序员仅需要继承这个类并且提供 entrySet 方法的实现即可。它将会返回一组 map 映射的某一段。通常,返回的集合将在AbstractSet 之上实现。这个set不应该支持 add 或者 remove 方法,并且它的迭代器也不支持 remove 方法。

为了实现可修改的 map,程序员必须额外重写这个类的 put 方法(否则就会抛出UnsupportedOperationException),并且 entrySet.iterator() 返回的 iterator 必须实现 remove() 方法。

Map 接口

Map 接口定义了 key-value 键值对的标准。一个对象支持 key-value 存储。Map不能包含重复的 key,每个键最多映射一个值。这个接口代替了Dictionary 类,Dictionary是一个抽象类而不是接口。
Map 接口提供了三个集合的构造器,它允许将 map 的内容视为一组键,值集合或一组键值映射。map的顺序定义为map映射集合上的迭代器返回其元素的顺序。一些map实现,像是TreeMap类,保证了map的有序性;其他的实现,像是HashMap,则没有保证。

重要内部类和接口

Node 接口

Node节点是用来存储HashMap的一个个实例,它实现了 Map.Entry接口,我们先来看一下 Map中的内部接口 Entry 接口的定义

Map.Entry

// 一个map 的entry 链,这个Map.entrySet()方法返回一个集合的视图,包含类中的元素,
// 这个唯一的方式是从集合的视图进行迭代,获取一个map的entry链。这些Map.Entry链只在
// 迭代期间有效。
interface Entry<K,V> {
  K getKey();
  V getValue();
  V setValue(V value);
  boolean equals(Object o);
  int hashCode();
}

Node 节点会存储四个属性,hash值,key,value,指向下一个Node节点的引用

KeySet 内部类

keySet 类继承于 AbstractSet 抽象类,它是由 HashMap 中的 keyset() 方法来创建 KeySet 实例的,旨在对HashMap 中的key键进行操作,看一个代码示例
在这里插入图片描述

图中把「1, 2, 3」这三个key 放在了HashMap中,然后使用 lambda 表达式循环遍历 key 值,可以看到,map.keySet() 其实是返回了一个 Set 接口,KeySet() 是在 Map 接口中进行定义的,不过是被HashMap 进行了实现操作,来看一下源码就明白了

// 返回一个set视图,这个视图中包含了map中的key。

public Set<K> keySet() {
  // // keySet 指向的是 AbstractMap 中的 keyset
  Set<K> ks = keySet;
  if (ks == null) {
    // 如果 ks 为空,就创建一个 KeySet 对象
    // 并对 ks 赋值。
    ks = new KeySet();
    keySet = ks;
  }
  return ks;
}


所以 KeySet 类中都是对 Map中的 Key 进行操作的:

在这里插入图片描述

Values 内部类

Values 类的创建其实是和 KeySet 类很相似,不过 KeySet 旨在对 Map中的键进行操作,Values 旨在对key-value 键值对中的 value 值进行使用,看一下代码示例:
在这里插入图片描述

循环遍历 Map中的 values值,看一下 values() 方法最终创建的是什么:


public Collection<V> values() {
  // values 其实是 AbstractMap 中的 values
  Collection<V> vs = values;
  if (vs == null) {
    vs = new Values();
    values = vs;
  }
  return vs;
}

所有的 values 其实都存储在 AbstractMap 中,而 Values 类其实也是实现了 Map 中的 Values 接口,看一下对 values 的操作都有哪些方法

在这里插入图片描述

其实是和 key 的操作差不多

EntrySet 内部类

上面提到了HashMap中分别有对 key、value 进行操作的,其实还有对 key-value 键值对进行操作的内部类,它就是 EntrySet,来看一下EntrySet 的创建过程:

在这里插入图片描述
点进去 entrySet() 会发现这个方法也是在 Map 接口中定义的,HashMap对它进行了重写

// 返回一个 set 视图,此视图包含了 map 中的key-value 键值对
public Set<Map.Entry<K,V>> entrySet() {
  Set<Map.Entry<K,V>> es;
  return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

如果 es 为空创建一个新的 EntrySet 实例,EntrySet 主要包括了对key-value 键值对映射的方法,如下

在这里插入图片描述

HashMap 1.7 的底层结构

JDK1.7 中,HashMap 采用位桶 + 链表的实现,即使用链表来处理冲突,同一 hash 值的链表都存储在一个数组中。但是当位于一个桶中的元素较多,即 hash 值相等的元素较多时,通过 key 值依次查找的效率较低。它的数据结构如下

在这里插入图片描述

HashMap 底层数据结构就是一个 Entry 数组,Entry 是 HashMap 的基本组成单元,每个 Entry 中包含一个 key-value 键值对。

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

而每个 Entry 中包含 「hash, key ,value」 属性,它是 HashMap 的一个内部类

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

所以,HashMap 的整体结构就

在这里插入图片描述

HashMap 1.8 的底层结构

与 JDK 1.7 相比,1.8 在底层结构方面做了一些改变,当每个桶中元素大于 8 的时候,会转变为红黑树,目的就是优化查询效率,JDK 1.8 重写了 resize() 方法。

在这里插入图片描述

HashMap 重要属性

「初始容量」

HashMap 的默认初始容量是由 DEFAULT_INITIAL_CAPACITY 属性管理的。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
HashMap 的默认初始容量是 1 << 4 = 16, << 是一个左移操作,它相当于是

在这里插入图片描述

「最大容量」

HashMap 的最大容量是

static final int MAXIMUM_CAPACITY = 1 << 30;
这里是不是有个疑问?int 占用四个字节,按说最大容量应该是左移 31 位,为什么 HashMap 最大容量是左移 30 位呢?因为在数值计算中,最高位也就是最左位的位 是代表着符号为,0 -> 正数,1 -> 负数,容量不可能是负数,所以 HashMap 最高位只能移位到 2 ^ 30 次幂。

「默认负载因子」

HashMap 的默认负载因子是

static final float DEFAULT_LOAD_FACTOR = 0.75f;
float 类型所以用 .f 为单位,负载因子是和扩容机制有关,这里大致提一下,后面会细说。扩容机制的原则是当 HashMap 中存储的数量 > HashMap 容量 * 负载因子时,就会把 HashMap 的容量扩大为原来的二倍。

HashMap 的第一次扩容就在 DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12 时进行。

「树化阈值」

HashMap 的树化阈值是

static final int TREEIFY_THRESHOLD = 8;
在进行添加元素时,当一个桶中存储元素的数量 > 8 时,会自动转换为红黑树(JDK1.8 特性)。

「链表阈值」

HashMap 的链表阈值是

static final int UNTREEIFY_THRESHOLD = 6;
在进行删除元素时,如果一个桶中存储元素数量 < 6 后,会自动转换为链表

「扩容临界值」

static final int MIN_TREEIFY_CAPACITY = 64;
这个值表示的是当桶数组容量小于该值时,优先进行扩容,而不是树化

「节点数组」

HashMap 中的节点数组就是 Entry 数组,它代表的就是 HashMap 中 「数组 + 链表」 数据结构中的数组。

transient Node<K,V>[] table;
Node 数组在第一次使用的时候进行初始化操作,在必要的时候进行 resize,resize 后数组的长度扩容为原来的二倍。

「键值对数量」

在 HashMap 中,使用 size 来表示 HashMap 中键值对的数量。

「修改次数」

在 HashMap 中,使用 modCount 来表示修改次数,主要用于做并发修改 HashMap 时的快速失败 - fail-fast 机制。

「扩容阈值」

在 HashMap 中,使用 threshold 表示扩容的阈值,也就是 初始容量 * 负载因子的值。

threshold 涉及到一个扩容的阈值问题,这个问题是由 tableSizeFor 源码解决的。我们先看一下它的源码再来解释

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;
}
代码中涉及一个运算符 |= ,它表示的是按位或,啥意思呢?你一定知道 「a+=b 的意思是 a=a+b」,那么同理:a |= b 就是 a = a | b,也就是双方都转换为二进制,来进行与操作。如下图所示

在这里插入图片描述
我们上面采用了一个比较大的数字进行扩容,由上图可知 2^29 次方的数组经过一系列的或操作后,会算出来结果是 2^30 次方。

所以扩容后的数组长度是原来的 2 倍。

「负载因子」

loadFactor 表示负载因子,它表示的是 HashMap 中的密集程度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值