目录
2.9,存储key=null的值putForNullKey()方法
2.13.3,containsKey(Object key)
2.13.7,containsValue(Object value)
2.13.8,keySet(),values(),entrySet()方法
4.3,通过HashMap的value来遍历HashMap集合
1,HashMap的介绍
1.1,HashMap的基本介绍
- HashMap 是一个散列表,它存储的内容是键值对(key-value)映射,它是通过“拉链法”解决哈希冲突的。
- 从上面的继承关系看,HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
- HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
- 通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
- HashMap是基于哈希表的Map接口的非同步实现,这意味着它不是线程安全的。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
1.2,HashMap的API
- HashMap的构造函数,在下面会对构造函数进行详细解释。
public HashMap(int initialCapacity, float loadFactor) {}
public HashMap(int initialCapacity) {}
public HashMap() {}
public HashMap(Map<? extends K, ? extends V> m) {}
- HashMap的API
public int size()
public boolean isEmpty()
public V get(Object key)
public boolean containsKey(Object key)
public V put(K key, V value)
public void putAll(Map<? extends K, ? extends V> m)
public V remove(Object key)
public void clear()
public boolean containsValue(Object value)
public Object clone()
public Set<K> keySet()
public Collection<V> values()
public Set<Map.Entry<K,V>> entrySet()
- HashMap的逻辑结构
在内存当中,我们创建大HashMap逻辑结构是下面这样,需要注意的是,里面存储的每一个数据都是EntrySet类型,也就是键值对。所以 HashMap的本质 = 1个存储Entry类对象的数组 + 多个单链表
2,jdk1.7中HashMap关键源码解读
2.1,jdk1.7中HashMap的定义
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable,Serializable{}
- 们可以看到HashMap继承于AbstractMap,实现了Map,Cloneable和Serializable接口。,
2.2,HashMap节点类型定义
- HashMap继承于AbstractMap类,实现了Map接口。Map是"key-value键值对"接口,AbstractMap实现了"键值对"的通用函数接口。
- HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
- 节点类型的实现:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;//键值
V value;//值
Entry<K,V> next;//指向下一个节点的指针
int hash;//哈希值
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判断两个Entry是否相等
// 若两个Entry的“key”和“value”都相等,则返回true。
// 否则,返回false
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
void recordAccess(HashMap<K,V> m) {
}
void recordRemoval(HashMap<K,V> m) {
}
}
- 我们可以看到HashMap节点实现Map里面的Entry接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数。这些都是基本的读取/修改key、value值的函数,也就是HashMap自己节点的类型。可以看出 Entry 实际上就是一个单向链表。这也是为什么我们说HashMap是通过拉链法解决哈希冲突的。
2.3,HashMap中重要的参数
//HashMap集合中数组的初始容量,以后的容量都要是2次幂数
// 1. 容量(capacity): HashMap中数组的长
// a. 容量范围:必须是2的幂 & <最大容量(2的30次方)
// b. 初始容量 = 哈希表创建时的容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//HashMap集合的最大容量,2的30次幂
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap底层树数组,数组存储元素是Entry类型
transient Entry<K,V>[] table;
//size代表整个hashmap中存储的元素个数,所以说包括链表元素
transient int size;
// 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)
// a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
// b. 扩容阈值 = 容量 x 加载因子
int threshold;
//实际的的加载因子
// 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
// a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
// b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
final float loadFactor;
/**
* 结构性变更的次数。
* 结构性变更是指map的元素数量的变化,比如rehash操作。
* 用于HashMap快速失败操作,比如在遍历时发生了结构性变更,就会抛出 ConcurrentModificationException。
*/
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
- 加载因子说明:
2.4,HashMap的构造函数
//参数:指定一个初始容量,加载因子使用默认值0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//无参构造函数,哈希表数组默认长度是16,默认加载因子是0.75
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//参数一:指定的初始容量
//参数二:指定的加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)//判断初始容量是否教育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;
// 设置 扩容阈值 = 初始容量
// 注:此处不是真正的阈值,是为了扩展table,该阈值后面会重新计算,下面会详细讲解
threshold = initialCapacity;
// 一个空方法用于未来的子对象扩展
init();
}
//构造包含子Map的HashMap,y也就是构造的HashMap传入的是键值对
//使用默认的加载因子0.75f,和默认容量16
public HashMap(Map<? extends K, ? extends V> m) {
//设置默认容量和加载因子
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//将传入的map添加到HashMap中
putAllForCreate(m);
}
- 此处仅用于接收初始容量大小(capacity)、加载因子(Load factor),但仍无真正初始化哈希表,即初始化存储数组table。为什么要这样做,也许是为了把哈希表初始化之后不用的话比较占用内存空间吧。
- 此处先给出结论:真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时。
2.5,put()方法(成对 放入 键 - 值对)
- put() 的作用是对外提供接口,让HashMap对象可以通过put()将“key-value”添加到HashMap中
public V put(K key, V value) {
// 1. 若哈希表未初始化(即 table为空)
// 则使用构造函数时设置的阈值(即初始容量)初始化数组table
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//判断key是否是空,也就是说hashmap中的key值可以是空
//若key == null,则将该键-值 存放到数组table 中的第1个位置,即table [0]
// (本质:key = Null时,hash值 = 0,故存放到table[0]中)
// 该位置永远只有1个value,新传进来的value会覆盖旧的value
if (key == null)
return putForNullKey(value);//此函数的功能是把key为null的对象放入数组下表为0的位置
int hash = hash(key);//获取键的哈希值
int i = indexFor(hash, table.length);//获取当前元素在哈希表中的下标,也就是存储位置下表
//对链表做遍历操作
//循环从当前链表的头结点开始,也就是数组的第i个位置,循环到链表的末尾
//为什么会有循环?
//因为put()方法是有返回值的
//既然做链表的遍历操作,为什么不直接把节点插入链表的尾部?
//因为链表的遍历不一定遍历到链表的尾部,有可能有相同的元素,然后就直接更新元素后就返回
//在链表的头部插入元素比较快,在这里循环遍历的目的就是查看是否插入相同的节点
//如果插入元素的时候,发现链表中有相同的key值,那么这个时候不需要考虑到底是头插法好还是尾插法比较好
//如果插入元素的时候,没有重复的key值,那么这个时候使用头插法和尾插法都一样,因为算法会遍历这个链表,哪种方式都一样
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断当前两个元素是否是相等的,如果相等,就做更新操作,在判断元素是否相等的时候,比较了两个元素key的哈希值是否一样
//键值是否一样,如果全部一样,就做更新操作
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//这个方法没有实现
e.recordAccess(this);
//更新完毕之后返回旧的值
return oldValue;
}
}
modCount++;
//hash是哈希值,i代表当前元素在数组中的下标,添加一个节点
//经过上面的判断,如果key不存在于当前的集合中,就添加到当前的集合
addEntry(hash, key, value, i);
return null;
}
- 若要添加到HashMap中的键值对对应的key已经存在HashMap中,则找到该键值对;然后新的value取代旧的value,并退出!
- 若要添加到HashMap中的键值对对应的key不在HashMap中,则将其添加到该哈希值对应的链表中,并调用addEntry()。
- 注:当发生 Hash冲突时,为了保证 键key的唯一性哈希表并不会马上在链表中插入新数据,而是先查找该 key是否已存在,若已存在,则替换即可,这个也是为什么我们的put()函数需要对链表遍历的原因,还有另一个原因就是方法有返回值。
- 还有一点这里使用的是头插法,也就是将待插入的对象放到数组中计算出来索引的位置,然后将当前元素的next域指向数组中原来的此位置的对象,从而保持链表不会断裂,在transfer()函数中转移节点使用的也是这种方式。
- 针对上面的put()方法,我们整体看看其过程:
2.6,addEntry()方法
- 此方法是向链表中添加一个节点。
//向链表中添加一个节点
//hash:元素的哈希码值,是根据hash()计算出来的
//key:entry的键值
//value:entry的value值
//bucketIndex:元素在数组中存储的下标索引
void addEntry(int hash, K key, V value, int bucketIndex) {
//扩容的第一个条件
//如果需要扩容,会先扩充容量,然后插入元素,首先比较数组中元素的个数和阈值的大小,阈值大小的计算方法:数组的大小乘以0.75
//阈值默认的大小是12
//扩容的第二个条件
//查看元素即将存放的位置是否是空,如果不空,才会进行扩容操作
if ((size >= threshold) && (null != table[bucketIndex])) {//查看当前集合中元素的个数是否超过阈值,并且判断当前元素存储的位置是否是空
resize(2 * table.length);//调用扩容操作,容量大小是原始数组容量大小的2倍
hash = (null != key) ? hash(key) : 0;//计算对象key的哈希码
bucketIndex = indexFor(hash, table.length);//根据数组长度和对象哈希码,计算对象在数组中存储的下标
}
// 创建一个对象
createEntry(hash, key, value, bucketIndex);
}
- addEntry()在添加一个节点的时候,会先判断当前的结合是否是需要进行扩容的,如果需要进行扩容,那么就先扩容,然后在调用createEntry()方法创建一个节点添加上去。
2.7,createEntry()方法
- addEntry() 的作用是新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
//创建一个节点
//hash:对象的哈希码
//key:对象的键值
//value:对象的value值
//bucketIndex:对象存储在数组中的下标索引
void createEntry(int hash, K key, V value, int bucketIndex) {
//保存数组中原有的元素到e中
Entry<K,V> e = table[bucketIndex];
//创建一个新的entry,并且放入bucketIndex位置
//然后把原来数组中的元素e连接到当前元素的后面
table[bucketIndex] = new Entry<>(hash, key, value, e);
//元素个数加1
size++;
}
//在这里我们看看Entry()的构造函数,next会指向传进来的n节点
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
- 比较上面两个方法,addEntry()方法是向链表中新增一个节点,而createEntry()是创建一个节点并添加到链表上面,在两个方法中,addEntry()方法多了容量判断的代码。也就是先判断当前集合中元素的个数是否超过设置的阈值,如果超过了设置的阈值,那么需要进行扩容判断。
if ((size >= threshold) && (null != table[bucketIndex])) {//查看当前集合中元素的个数是否超过阈值,并且判断当前元素存储的位置是否是空
resize(2 * table.length);//调用扩容操作,容量大小是原始数组容量大小的2倍
hash = (null != key) ? hash(key) : 0;//计算对象key的哈希码
bucketIndex = indexFor(hash, table.length);//根据数组长度和对象哈希码,计算对象在数组中存储的下标
}
- 那它们的区别到底是什么呢?阅读代码,我们可以发现,它们的使用情景不同。
- addEntry()一般用在 新增Entry可能导致“HashMap的实际容量”超过“阈值”的情况下。
- 例如,我们新建一个HashMap,然后不断通过put()向HashMap中添加元素;put()是通过addEntry()新增Entry的。
- 在这种情况下,我们不知道何时“HashMap的实际容量”会超过“阈值”,因此,需要调用addEntry()
- createEntry() 一般用在 新增Entry不会导致“HashMap的实际容量”超过“阈值”的情况下。
- 例如,我们调用HashMap“带有Map”的构造函数,也就是我们如果想把一个集合中的元素全部添加到HashMap中,调用构造函数HashMap(Map<? extends K, ? extends V> m),在他的底层使用的是CreateEntry()函数,它绘将Map的全部元素添加到HashMap中;
-
但在添加之前,我们已经计算好“HashMap的容量和阈值”。也就是,可以确定“即使将Map中的全部元素添加到HashMap中,都不会超过HashMap的阈值”。此时,调用createEntry()即可。
- addEntry()一般用在 新增Entry可能导致“HashMap的实际容量”超过“阈值”的情况下。
2.8,初始化哈希表inflateTable()方法
- 上面我们已经说了,哈希表数组的初始化与分配并不是在构造函数中,而是在一个叫做inflateTable的方法中。
- 即初始化数组(table)、扩容阈值(threshold)
private void inflateTable(int toSize) {
// 1. 将传入的容量大小转化为:>传入容量大小的最小的2的次幂
// 即如果传入的是容量大小是19,那么转化后,初始化容量大小为32(即2的5次幂)
int capacity = roundUpToPowerOf2(toSize);
//2. 重新计算阈值 threshold = 容量 * 加载因子
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 3. 使用计算后的初始容量(已经是2的次幂) 初始化数组table(作为数组长度)
// 即 哈希表的容量大小 = 数组大小(长度)
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
/**
* 分析:roundUpToPowerOf2(toSize)
* 作用:将传入的容量大小转化为:>传入容量大小的最小的2的幂
* 特别注意:容量大小必须为2的幂,该原因在下面的讲解会详细分析
*/
private static int roundUpToPowerOf2(int number) {
//若 容量超过了最大值,初始化容量设置为最大值 ;否则,设置为:>传入容量大小的最小的2的次幂
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
- 真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时,在第一次调用put的方法的时候,会先判断当前的表是否适合空,也就是是否进行初始化,如果没有,就会调用此方法进行初始化操作。
2.9,存储key=null的值putForNullKey()方法
- 当 key ==null时,将该 key-value 的存储位置规定为数组table 中的第1个位置,即table [0]
//如果key=0,那么就会把key等于0对应的值存储在数组的索引为0的位置
private V putForNullKey(V value) {
//遍历链表是因为可能存在某一个key值对应的哈希值也为0
// 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对
// 1. 若有:则用新value 替换 旧value;同时返回旧的value值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// addEntry()的第1个参数 = hash值 = 传入0,也就是null对应的哈希值为0
//即 说明:当key = null时,也有hash值 = 0,所以HashMap的key 可为null
//对比HashTable,由于HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
//此处只需知道是将 key-value 添加到HashMap中即可,关于addEntry()的源码分析将等到下面再详细说明,
addEntry(0, null, value, 0);
return null;
}
- 注:
- HashMap的键key 可为null(区别于 HashTable的key 不可为null)
- HashMap的键key 可为null且只能为1个,但值value可为null且为多个
2.10,计算对象在数组中的下标
- 计算存放数组 table 中的位置(即 数组下标 or 索引)
int hash = hash(key);//获取键的哈希值
int i = indexFor(hash, table.length);//获取当前元素在哈希表中的下标,也就是存储位置下表
/*
* 源码分析1:hash(key)
* 该函数在JDK 1.7 和 1.8 中的实现不同,但原理一样 = 扰动函数 = 使得根据key生成的哈希码(hash值)分布更加均匀、
更具备随机性,避免出现hash值冲突(即指不同key但生成同1个hash值)
* JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
* JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
*/
// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
final int hash(Object k) {
//获取哈希种子,哈希种子默认初始值是0
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//获取键值的哈希码,和哈希种子做异或操作
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
// 1. 取hashCode值: h = key.hashCode()
// 2. 高位参与低位的运算:h ^ (h >>> 16)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// a. 当key = null时,hash值 = 0,所以HashMap的key 可为null
// 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
// b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
}
//参数一:对象哈希码
//擦参数二:哈希表中数组的长度
static int indexFor(int h, int length) {
//将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
return h & (length-1);
}
- 为什么不直接使用对象计算出来的HashCode()作为数组的下标进行存储?
- 因为我们的每一个对象都是根据对象在内存中的地址计算出来的一个int整形数值,这个数字一般比较大,如果直接采用这个数字,那么我们需要的哈希表数组会比较大,(也就是说为了使得我们的对象在数组中的存储地址和对象计算出来的哈希码匹配,我们一般不直接使用对象的HashCode作为存储下标)。
- 为什么采用对象的哈希码和数组长度值-1做&运算来计算对象在数组中的存储下标?
- 根据HashMap的容量大小(数组长度),按需取哈希码一定数量的低位作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配” 的问题。
- 为什么在计算对象的下标之前需要对对象的哈希码做二次处理,也就是扰动处理?
- 加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突
注:所有处理的根本目的,都是为了提高 存储key-value的数组下标位置 的随机性 & 分布均匀性,尽量避免出现hash值冲突。即:对于不同key,存储的数组下标位置要尽可能不一样
2.11,哈希表的扩容机制
//参数标示新的扩容容量
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(newTable, initHashSeedAsNeeded(newCapacity));
//指向新数组的引用
table = newTable;
//重新计算阈值,新数组的容量*加载因子
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
- 在resize()方法中,真正完成扩容的过程的是transfer()方法。在这个方法中,会把就数组上连接的节点重新根据新数组的大小计算对象的下标的值进行重新存储。
2.12,扩容的过程
//把旧数组中的全部元素移动到新的数组中
//参数一:新的数组的引用
//参数二:标志位,标示是否需要在计算hash值
void transfer(Entry[] newTable, boolean rehash) {
//获取新数组的长度
int newCapacity = newTable.length;
//循环遍历数组中的每一个元素,也就是对数组中每一条链表进行遍历
for (Entry<K,V> e : table) {
//如果数组中当前位置不为空,也就是当前链表中存储有元素
while(null != e) {
//如果数组中元素e不是空的,就新开辟一个节点,指向当前数组中对应于链表的第一个元素
Entry<K,V> next = e.next;
//标示是否需要重新计算元素在数组中的下标
if (rehash) {
//hash存储的是对象的哈希码,而不是在数组中的存储下标
e.hash = null == e.key ? 0 : hash(e.key);
}
//在这里,元素的hash值是没有变化的,变化的是数组的长度,所以会根据数组的长度取计算一个新的下标
//但是这个下标只有两种可能,要么不变,要么就是此时的下标加上原始数组的容量大小也就是当前位置+oldTable.length
//这种操作的前提条件是没有在HASH操作,也就是没有进入上面的判断语句
//注意:在这里使用的是头插法插入新节点
int i = indexFor(e.hash, newCapacity);//根据元素的哈希码,数组的新的容量,计算对象在数组中存储的下标
e.next = newTable[i];//把老数组e的下一个元素指向新数组计算出下标的位置处
newTable[i] = e;
e = next;//e重新指向下一个元素
//但是在这里有一个问题,为何不直接把链表连接到新的数组上面,为何一个一个节点移动
//因为一个链表上面的所有节点不一定会重新全部映射到新数组的一条链表上面,可能会映射到别的链表上面
//这样可以减短链表的长度,提高定位的效率
}
}
}
2.12,其他方法说明
2.13.1,isEmpty()
isEmpty()函数的作用是判断集合是否是空。
public boolean isEmpty() {
return size == 0;
}
2.13.2,get(Object key)
get() 的作用是获取key对应的value,它的实现代码如下:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//获取key对应的哈希码
int hash = (key == null) ? 0 : hash(key);
//遍历链表,查找待查找的对象
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
2.13.3,containsKey(Object key)
containsKey() 的作用是判断HashMap是否包含key。
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
2.13.4,putAll()
putAll() 的作用是将"m"的全部元素都添加到HashMap中,它的代码如下:
public void putAll(Map<? extends K, ? extends V> m) {
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return;
if (table == EMPTY_TABLE) {
inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
}
// 计算容量是否足够,
// 若“当前实际容量 < 需要的容量”,则将容量x2。
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length)
resize(newCapacity);
}
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
2.13.5,remove()
remove() 的作用是删除“键为key”元素
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
// 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算
int hash = (key == null) ? 0 : hash(key);
//获取对象在数组中的存储下标
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
// 删除链表中“键为key”的元素
// 本质是“删除单向链表中的节点”
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
2.13.6,clear()
clear() 的作用是清空HashMap。它是通过将所有的元素设为null来实现的。
public void clear() {
modCount++;
Arrays.fill(table, null);
size = 0;
}
2.13.7,containsValue(Object value)
containsValue() 的作用是判断HashMap是否包含“值为value”的元素。
public boolean containsValue(Object value) {
//如果值为Null,就调用containsNullValue()方法
if (value == null)
return containsNullValue();
Entry[] tab = table;
//遍历哈希表
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
//查找值为null的键值对
private boolean containsNullValue() {
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (e.value == null)
return true;
return false;
}
2.13.8,keySet(),values(),entrySet()方法
上面三个方法类似,都是用于遍历HashMap集合使用的。
//返回的是一个Set结合
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
//返回的是一个Collection集合
public Collection<V> values() {
Collection<V> vs = values;
return (vs != null ? vs : (values = new Values()));
}
//返回的是HashMap节点类型的一个集合
public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
- 下面我们看看是如果通过EntrySet对HashMap进行遍历的。
public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
//在这里返回的是一个EntrySet对象
private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
return es != null ? es : (entrySet = new EntrySet());
}
// EntrySet继承于AbstractSet,说明该集合中没有重复的EntrySet。
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
//返回一个迭迭代器
public Iterator<Map.Entry<K,V>> iterator() {
return newEntryIterator();
}
//判断是否包含对象o
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<K,V> e = (Map.Entry<K,V>) o;
Entry<K,V> candidate = getEntry(e.getKey());
return candidate != null && candidate.equals(e);
}
//删除一个对象
public boolean remove(Object o) {
return removeMapping(o) != null;
}
//返回集合的大小
public int size() {
return size;
}
//清除集合的元素
public void clear() {
HashMap.this.clear();
}
}
- 通过获取EntrySet的迭代器进行遍历操作
//抽象的迭代器,实现了Iterator接口
// HashIterator是HashMap迭代器的抽象出来的父类,实现了公共了函数。
// 它包含“key迭代器(KeyIterator)”、“Value迭代器(ValueIterator)”和“Entry迭代器(EntryIterator)”3个子类。
private abstract class HashIterator<E> implements Iterator<E> {
Entry<K,V> next; // 下一个元素
int expectedModCount; // expectedModCount用于实现fast-fail机制。
int index; // 当前索引
Entry<K,V> current; // 当前元素
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
// 将next指向table中第一个不为null的元素。
// 这里利用了index的初始值为0,从0开始依次向后遍历,直到找到不为null的元素就退出循环。
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
public final boolean hasNext() {
return next != null;
}
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
// 注意!!!
// 一个Entry就是一个单向链表
// 若该Entry的下一个节点不为空,就将next指向下一个节点;
// 否则,将next指向下一个链表(也是下一个Entry)的不为null的节点。
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
}
private final class ValueIterator extends HashIterator<V> {
public V next() {
return nextEntry().value;
}
}
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
// Subclass overrides these to alter behavior of views' iterator() method
Iterator<K> newKeyIterator() {
return new KeyIterator();
}
Iterator<V> newValueIterator() {
return new ValueIterator();
}
Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator();
}
当我们通过entrySet()获取到的Iterator的next()方法去遍历HashMap时,实际上调用的是 nextEntry() 。而nextEntry()的实现方式,先遍历Entry(根据Entry在table中的序号,从小到大的遍历);然后对每个Entry(即每个单向链表),逐个遍历。
3,HashMap实现的接口
3.1,实现Cloneable接口
HashMap实现了Cloneable接口,即实现了clone()方法。
clone()方法的作用很简单,就是克隆一个HashMap对象并返回。
public Object clone() {
HashMap<K,V> result = null;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
}
//对result数组进行初始化操作,调用的是函数inflateTable()
if (result.table != EMPTY_TABLE) {
result.inflateTable(Math.min(
(int) Math.min(
size * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY),
table.length));
}
result.entrySet = null;
result.modCount = 0;
result.size = 0;
result.init();
// 调用putAllForCreate()将全部元素添加到HashMap中
result.putAllForCreate(this);
return result;
}
3.2,实现的Serializable接口
// java.io.Serializable的写入函数
// 将HashMap的“总的容量,实际容量,所有的Entry”都写入到输出流中
private void writeObject(java.io.ObjectOutputStream s)
throws IOException
{
Iterator<Map.Entry<K,V>> i =
(size > 0) ? entrySet0().iterator() : null;
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
// Write out number of buckets
s.writeInt(table.length);
// Write out size (number of Mappings)
s.writeInt(size);
// Write out keys and values (alternating)
if (i != null) {
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
s.writeObject(e.getKey());
s.writeObject(e.getValue());
}
}
}
// java.io.Serializable的读取函数:根据写入方式读出
// 将HashMap的“总的容量,实际容量,所有的Entry”依次读出
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold, loadfactor, and any hidden stuff
s.defaultReadObject();
// Read in number of buckets and allocate the bucket array;
int numBuckets = s.readInt();
table = new Entry[numBuckets];
init(); // Give subclass a chance to do its thing.
// Read in size (number of Mappings)
int size = s.readInt();
// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<size; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}
4,HashMap的遍历操作
4.1,通过EntrySet键值对进行遍历
- 第一步:根据entrySet()获取HashMap的“键值对”的Set集合。
- 第二步:通过for-each循环或者迭代器对Set集合进行遍历
public class Test3 {
public static void main(String[] args) {
// 1 声明一个HashMap对象
Map<String,Integer> hashMap=new HashMap<String, Integer>();
// 2 向hashmap中添加对象,以键值对的方式进行添加
hashMap.put("java",1);
hashMap.put("python",2);
hashMap.put("机器学习",3);
hashMap.put("ios",4);
hashMap.put("c++",5);
// 1 获得key-value对(Entry)的Set集合
Set<Map.Entry<String, Integer>> set=hashMap.entrySet();
// 2 遍历set集合,获取key值和value值,
// 第一种方式遍历:使用for-each方式进行遍历
for(Map.Entry<String ,Integer> entry:set){
System.out.println(entry.getKey()+" "+entry.getValue());
}
// 第二种遍历entry方式:通过迭代器,获取entry的迭代器,然后循环遍历
Iterator iterator=set.iterator();
// 通过循环遍历
while (iterator.hasNext()){
// 遍历的时候需要先获取entry,然后在获取键和值
Map.Entry entry1=(Map.Entry)iterator.next();
System.out.println(entry1.getKey()+" "+entry1.getValue());
}
}
}
4.2,通过HashMap的键来遍历HashMap
- 第一步:根据keySet()获取HashMap的“键”的Set集合。
- 第二步:通过迭代器或者循环来遍历第一步获取的集合。
public class Test3 {
public static void main(String[] args) {
// 1 声明一个HashMap对象
Map<String,Integer> hashMap=new HashMap<String, Integer>();
// 2 向hashmap中添加对象,以键值对的方式进行添加
hashMap.put("java",1);
hashMap.put("python",2);
hashMap.put("机器学习",3);
hashMap.put("ios",4);
hashMap.put("c++",5);
/// 获取key的set集合,通过遍历key的set集合来获取值
// 1 获取key的set集合
Set<String> set1=hashMap.keySet();
// 2 通过遍历key来获取value
// 第一种遍历方式:通过for-each循环
for(String key:set1){
System.out.println(hashMap.get(key));
}
// 第二种方式遍历key的set集合,通过获取key的迭代器遍历
Iterator iterator1=set1.iterator();
String key=null;
while (iterator1.hasNext()){
key=(String)iterator1.next();
System.out.println(hashMap.get(key));
}
}
}
4.3,通过HashMap的value来遍历HashMap集合
- 第一步:根据value()获取HashMap的“值”的集合。
- 第二步:通过循环或者迭代器遍历HashMap集合。
public class Test3 {
public static void main(String[] args) {
// 1 声明一个HashMap对象
Map<String,Integer> hashMap=new HashMap<String, Integer>();
// 2 向hashmap中添加对象,以键值对的方式进行添加
hashMap.put("java",1);
hashMap.put("python",2);
hashMap.put("机器学习",3);
hashMap.put("ios",4);
hashMap.put("c++",5);
// 获取value的set集合。然后再遍历
// 1 获取values
Collection valueSet=hashMap.values();
// 2 获取values的迭代器
Iterator iterator2=valueSet.iterator();
while (iterator2.hasNext()){
System.out.println(iterator2.next());
}
}
}
- 注意:
-
对于遍历方式,推荐使用针对 key-value对(Entry)的方式:效率高。
-
对于 遍历keySet 、valueSet,实质上 = 遍历了2次:1 = 转为 iterator 迭代器遍历、2 = 从 HashMap 中取出 key 的 value 操作(通过 key 值 hashCode 和 equals 索引)
-
对于 遍历 entrySet ,实质 = 遍历了1次 = 获取存储实体Entry(存储了key 和 value )。
-
参考资料:
[1] https://www.cnblogs.com/skywang12345/p/3310835.html#a23
[2] https://www.jianshu.com/p/068b6d25b593