java集合(4):HashMap源码分析(jdk1.8)

时间:2021-12-31 19:17:54

前言

  • Map接口虽然也是集合体系中的重要一个分支,但是Map接口并不继承自Collection,而是自成一派。
public interface Map<K,V>
  • Map集合存储键映射到值的对象。一个集合中不能包含重复的键,每个键最多只能映射到一个值。

  • Map 接口提供三种collection 视图(意思是3种迭代器),允许以键集、值集或键-值映射关系集的形式来迭代某个Map的内容。映射顺序 定义为迭代器在映射的 collection 视图上返回其元素的顺序。某些映射实现可明确保证其顺序,如 TreeMap 类;另一些映射实现则不保证顺序,如 HashMap 类。

  • Map接口最常用的两个实现类就是HashMap和TreeMap。前者以哈希表结构存储元素,后者以二叉树结构存储元素,而且Set接口下的HashSet底层结构就是HashMap,所以讲完HashMap再讲HashSet就很简单了。

正文

一,哈希表结构

  • 哈希表结构是数据结构中的重要概念之一,其原理不再赘述,本文重点介绍哈希表结构的代码体现。需要说明的一点是:jdk1.8以前的HashMap只使用了数组和链表的形式,这样可能导致数组某个位置上的链表过长,不利于查询。jdk1.8以后,如果某位置的链表超过一定长度就会转为红黑树,这样就有利于查询。

二,HashMap源码

1, 类名及常量

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {


private static final long serialVersionUID = 362498820763181265L;

// Map集合初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// Map集合最大容量
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;

//
static final int MIN_TREEIFY_CAPACITY = 64;
}

2,成员变量

    // 存储元素的数组,每个元素都是Node型的,Node是一个静态内部类
transient Node<K,V>[] table;

// entrySet()方法返回的结果集
transient Set<Map.Entry<K,V>> entrySet;

// Map元素个数
transient int size;

// Map对象被修改的次数
transient int modCount;

// 增加数组容量的阈值
int threshold;

// 加载因子
final float loadFactor;

3,Node类型元素的内部类形式

static class Node<K,V> implements Map.Entry<K,V> { // 实现Map接口的内部接口Entry
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;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

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

4,HashMap的4个构造方法

    public HashMap() {  // 1,无参构造方法,成员变量使用默认值
this.loadFactor = DEFAULT_LOAD_FACTOR;
}

public HashMap(int initialCapacity) { // 2,初始容量的构造方法
this(initialCapacity, DEFAULT_LOAD_FACTOR); // 去调用2个参数的构造方法
}

public HashMap(int initialCapacity, float loadFactor) { // 3,指定容量和加载因子
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;

// 调用计算容量的方法,返回大于initialCapacity的最小2的幂。
this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(Map<? extends K, ? extends V> m) { // 4,接收一个map对象
this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认加载因子
putMapEntries(m, false); // 添加m到结构
}

5,put()方法详解,该方法也是最重要的方法,下面是方法的执行流程图和源码分析

java集合(4):HashMap源码分析(jdk1.8)

执行过程:

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

put方法源码:

    public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); // 调用putVal方法
}

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,则进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;

// 如果节点hash值对应的数组位置为空,直接赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);


else {
Node<K,V> e; K k;

// 如果节点hash值相同且key相同,则直接覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;

// 判断节点是否为树节点,如果是,则按红黑树的插入方式插入元素
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

// 按链表方式插入节点
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);

// 如果链表长度大于8,则将链表转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}

// 如果key存在,直接覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // map结构被修改次数自增
if (++size > threshold) // 添加元素后,如果数组长度超过阈值,则扩容。
resize();
afterNodeInsertion(evict);
return null;
}

5.1,扩容机制

  • 扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

  • 数组的容量是2的次幂,也就是说每次扩展,长度扩展为原来的2倍。接着元素也要变换位置,那么,元素的位置要么是在原位置,要么是在原位置加上2的次幂的位置。所以,我们在扩充的时候,不需要再重新计算hash值,而是看原先的hash值新增的1bit是1还是0。如果是0,索引不变,元素待在原位;如果是1,新索引为“原索引+扩展长度”。这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。

  • 扩容方法resize()
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 当前数组赋值给oldTab
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; // 当前阈值赋值给oldThr
int newCap, newThr = 0; // 新数组容量,新阈值
if (oldCap > 0) {

// 如果超过最大容量,就不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}

// 如果老容量扩大2倍仍不超过最大值,则新容量为原来的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 { // 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;
@SuppressWarnings({"rawtypes","unchecked"})
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;

// 如果e是树节点,则按照树结构处理该分支
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

// 如果e是链表节点,则按照链表结构处理该分支
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;
}

6,get()方法详解,get的过程就是遍历的过程。

    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; // tab是table的副本
Node<K,V> first, e; // first代表数组上链表的第一个元素
int n;
K k; // k是first元素的key属性副本

// 如果数组不为空,数组长度大于0,数组上链表第一个元素不为空,则进行遍历
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {

// 如果参数的hash值和key与first的hash值和key相同,则返回first
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;
}

7,keySet()和entrySet()方法,

  • Map本身没有迭代器,若要迭代,需要将Map转化为Set集合,使用Set集合的迭代器进行迭代。如下图,这两个方法均返回一个Set集合对象,分别将key和(key,value)作为整体构成一个Set。

java集合(4):HashMap源码分析(jdk1.8)

方法源码:

    public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet(); // KetSet是个内部类,继承自Set体系,所以有迭代器方法
keySet = ks;
}
return ks;
}

public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;

// EntrySet也是内部类,同样继承自Set体系,有增强的迭代器方法
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

这两个方法常见用法:

package com.jimmy.map;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class MapDemo1 {
public static void main(String[] args) {

Map<String, String> map = new HashMap<>();
System.out.println(map.put("name1", "jimmy"));
System.out.println(map.put("name2", "jimmy"));
System.out.println(map.put("name3", "jimmy"));

Set<String> keySet = map.keySet(); // keySet()方法返回map集合中所有key的set集合
for (String eachKey : keySet) {
System.out.println(eachKey+"::"+map.get(eachKey));
}

System.out.println("------------------");
// entrySet()方法返回map集合中(k,v)对的set集合
Set<Entry<String, String>> entrySet = map.entrySet();
for (Entry<String, String> eachMap : entrySet) {
System.out.println(eachMap.getKey()+"--"+eachMap.getValue());
}
}
}

总结

map结构相对复杂,其实现类按照不同的数据结构(哈希表结构或者树结构)来存储元素,应该在学习了相应数据结构的基础上再学习相应的集合实现。