目录
类图

图解

Map
特点
键值对的存储
元素需要重写hashCode()和equals()
api
增加:put(K key, V value);
删除:clear(); remove(Object key);
查看:get(Object key); size(); entrySet(); keySet(); values();
判断:containsKey(Object key);containsValue(Object value);
equals(Object o); isEmpty();
AbstractMap
特点
- 元素无序、唯一
- 底层结构:哈希表(数组 + 链表)
HashMap
特点
线程不安全,效率高
key可以存入null,并且唯一
属性
// 数组初始化长度 ==> 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 数组最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 负载因子/加载因子的默认值:0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 底层主数组
transient Entry<K,V>[] table;
// 元素的数量
transient int size;
// 数组扩容的边界值/门槛值,默认为 0
int threshold;
// 装填因子、负载因子、加载因子,影响扩容边界值
final float loadFactor;
内部类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
负载因子为什么是0.75
数组扩容边界值 = 数组长度 * 负载因子
- 如果设置为1:空间利用率高;但哈希碰撞的几率也变高,而链表的查询效率较低
- 如果设置为0.5:哈希碰撞的几率低,产生链表的几率低,查询效率高;但扩容频繁,空间利用率低
- 所以在 0.5 ~ 1 之间,取中间值:0.75
JDK1.7
工作原理

- 元素:
key,value - 调用key的hashCode(),得到
hash值,hash值对数组table长度进行取余hash & (length - 1) 等效于 hash % length(& 的效率比 % 要高),得到元素在数组中的位置(i) - 通过节点的next属性,遍历table[i]上链表的节点e
- 比较
e.key 和 key是否相等- e.hash == hash:首先判断hash值,hash不相等,则key一定不相等
- e.key == key:判断key的地址值,地址值一样,则key一定相等
- key.equals(e.k):最后调用equals() 判断key 是否相等
- 若相等,value替换oldValue,并
return oldValue - 判断是否需要
扩容 - 采用头插法,获取table[i]上的节点e,封装元素
new Entry(hash, k, v, e)next指向e,table[i]重新指向新节点,成为新的头节点 return null
构造函数
// 无参构造
public HashMap() {
// this(16, 0.75)
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
// 左移1位,等同于 * 2
// 确保数组的长度为 2的整数倍
capacity <<= 1;
// 确定使用的负载因子
this.loadFactor = loadFactor;
// threshold = capacity * loadFactor = 16 * 0.75 = 12;
// 确定数组扩容的边界值:12
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 创建主数组,长度为16
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
- 默认数组长度为16,负载因子为0.75
- 调整数组长度,变为2的整数倍
- 确定数组扩容边界值:
数组长度 * 负载因子 - 创建指定长度的Entry数组table
为什么数组长度必须是2的整数倍?
h & (length - 1) 等效于 h % length的前提就是length == 2^n- 减少哈希冲突
put()
添加元素
public V put(K key, V value) {
// 对key == null处理,允许key的值为空
if (key == null)
return putForNullKey(value);
// 获取key的哈希值
int hash = hash(key);
// 得到元素在数组中的位置
int i = indexFor(hash, table.length);
// 哈希碰撞
// 对table[i]上的链表元素进行循环,key相等则替换value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// e.hash == hash:首先判断hash值,hash不相等,则key一定不相等
// e.key == key:判断key的地址值,地址值一样,则key一定相等
// key.equals(k):最后调用equals() 判断key 是否相等
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
// 获取原value
V oldValue = e.value;
// 替换成新value
e.value = value;
e.recordAccess(this);
// 返回原value
return oldValue;
}
}
modCount++;
// 添加元素
addEntry(hash, key, value, i);
return null;
}
hash()
计算key的哈希值
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
// 二次散列,没有直接使用hashCode()的值,减少哈希冲突
h ^= k.hashCode();
// 扰动函数--核心:增加值的不确定性
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
indexFor()
计算元素对应数值的位置
static int indexFor(int h, int length) {
// 计算元素对应数组位置的公式
// 等效 h % length(前提:length为2的整数倍),&的效率高
return h & (length-1);
}
addEntry()
创建、添加节点
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果元素总个数大于等于数组扩容的临界值 && 添加的位置已有元素
// 则进行数组扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容为原数组长度的2倍
resize(2 * table.length);
// 扩容后重新计算hash值
hash = (null != key) ? hash(key) : 0;
// 重新计算存放数组位置
bucketIndex = indexFor(hash, table.length);
}
// 创建节点
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
// 头插法
// 获取数组位置的节点 e
Entry<K,V> e = table[bucketIndex];
// 创建新节点,并且next指向 e
// 新节点赋值到 数组位置节点,成为该数组位置新的头节点
table[bucketIndex] = new Entry<>(hash, key, value, e);
// 元素总个数 + 1
size++;
}
resize()
扩容:原数组长度的2倍
resize(2 * table.length)
void resize(int newCapacity) {
// 获取原数组
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果原数组的长度已达最大容量,则放弃扩容
if (oldCapacity == MAXIMUM_CAPACITY) {
// 扩容临界值设置为Integer的最大值
threshold = Integer.MAX_VALUE;
return;
}
// 根据指定容量,创建新的数组
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
// 把原数组的元素(所有头节点)复制到新的数组上
transfer(newTable, rehash);
// 新数组替换原数组
table = newTable;
// 重新计算数组扩容临界值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
JDK1.8
构造函数
无参构造
负载因子默认为 0.75; 其他属性均为默认值
// 无参构造
public HashMap() {
// 负载因子默认为 0.75; 其他属性均为默认值
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
有参构造
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);
}
// 返回最接近 参数的2的n次幂
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;
}
put()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hash():计算key的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab:底层数组table
// p:tab[i]的元素节点
// n:底层数组table的长度length
// i:元素存放数组下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 一、是否需要初始化
if ((tab = table) == null || (n = tab.length) == 0)
// 1.table为null或者长度为 0,resize()进行初始化
// 扩容后的数组和数组长度分别重新赋值到 tab 和 n
n = (tab = resize()).length;
// 二、插入元素
// (n - 1) & hash,等同 hash % n,计算存放数组下标
if ((p = tab[i = (n - 1) & hash]) == null)
// 2.1.如果tab[i] == null,创建新节点,并赋值到tab[i]作为头节点
tab[i] = newNode(hash, key, value, null);
else {
// 2.2.如果tab[i] != null,即哈希冲突
// e:存放集合中与添加元素的key相等的元素
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 解决哈希冲突1:如果添加元素的key 与 p节点(即tab[i]) 的key相等,
// p赋值给e节点
e = p;
else if (p instanceof TreeNode)
// 解决哈希冲突2:如果p节点属于树节点,则通过putTreeVal()添加节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 解决哈希冲突3:遍历tab[i]所在的链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// p.next == null,尾插法
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 如果链表长度 >= 7,则尝试进行树化
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 如果链表上存在元素的key与添加元素的key相等,终端循环
break;
p = e;
}
}
// 3.解决哈希冲突过程中,如果e != null,
// 则表示集合中存在key与添加元素的key相等,需要替换value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 三、元素插入成功,后续处理
++modCount;
// 添加后如果size大于扩容临界值,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- 判断table是否为null或者长度为 0,
resize()对table进行初始化,长度为n - (n - 1) %hash取余,计算元素存放数组下标i
- tab[i]如果为null,创建新节点,并赋值到tab[i]作为头节点
- 否则即是发生哈希冲突,准备
变量e:记录key相等的节点- 如果添加元素的key 与 tab[i] 的key相等(hash, 地址值, equals),tab[i]赋值给e
- 如果tab[i]属于树节点,调用
putTreeVal()添加节点 - 遍历tab[i]所在链表
- 当前节点的key与元素key相等,赋值给e,跳出循环
- 使用尾插法,通过next,遍历找到尾节点:next == null,将元素封装成节点,赋值给next;同时如果链表长度 >= 7,则尝试进行树化,
treeifyBin()
- 如果
e != null,表示存在节点key与元素key相等,元素value替换节点value,并返回oldValue
- ++size
- 判断是否需要进行扩容,
resize()
resize()
final Node<K,V>[] resize() {
// 获取原数组、原长度、原扩容边界值
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
// 新长度、新扩容边界值 默认为 0
int newCap, newThr = 0;
// =========================确定新长度、新边界值start==============
if (oldCap > 0) { // 第n次扩容:原长度大于0
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果原长度大于等于最大长度,调整扩容边界值为Integer.max,返回原数组
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新长度 = 原长度的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 如果新长度小于最大长度并且原长度大于等于默认长度16
// 新扩容边界值 = 原扩容边界值的2倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 有参第一次扩容:如果原长度 == 0 && 原边界值 > 0
// 新长度 = 原边界值
newCap = oldThr;
else { // 无参第一次扩容:原长度 == 0 && 原边界值 == 0
// 新长度 = 默认值16
// 新边界值 = 默认值:0.75 * 16 = 12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新边界值 == 0 (有参第一次扩容)
if (newThr == 0) {
// 新边界值 = Min(新长度 * 负载因子, Integer.max)
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;
// ==========================确定新长度、新边界值end===============
if (oldTab != null) {
// 如果原数组 != null,遍历数组上的每个元素oldTab[j],复制到newTab中
for (int j = 0; j < oldCap; ++j) {
// e:原数组上的节点:oldTab[j]
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 如果e所在链只有自己一个节点,
// 直接重新计算下标,放入到新数组中
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;
}
- 确定新长度、新边界值
- 原长度 > 0 :不是第一次扩容
- 新长度为原长度的2倍
- 如果
新长度小于最大长度 并且 原长度大于等于默认长度16,那么 新边界值为原边界值的2倍
- 原长度 == 0 && 原边界值 > 0:有参的第一次扩容
新长度 = 原边界值
- 原长度 == 0 && 原边界值 == 0:无参的第一次扩容
- 新长度 = 16
- 新边界 = 0.75 * 16 = 12
- 如果新边界值 == 0,
新边界值 = Min(新长度 * 负载因子, Integer.max)
- 原长度 > 0 :不是第一次扩容
- 如果原数组 != null,遍历数组上的每个元素oldTab[j],复制到newTab中
- 如果oldTab[j]所在链表只有自己一个元素,则重新计算下标,放入到新数组中
treeifyBin()
树化
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)
// 如果 数组 == null || 长度 < 64,进行扩容,放弃树化
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 长度 >= 64 && tab[index] != null,进行树化
// 1.单向链表转化为双向链表
// hd:头节点
// tl:尾节点
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
// 2.双向链表转为红黑树
hd.treeify(tab);
}
}
- 如果 数组 == null || 长度 < 64,进行扩容,放弃树化
- 树化
- 单向链表转为双向链表
- 双向链表转为红黑树
HashTable
特点
线程安全,效率低
key不可以存入null
TreeMap
特点
唯一、升序
底层:红黑树
放入集合的key必须实现比较器(内部或外部比较器)
属性
// key的外部比较器
private final Comparator<? super K> comparator;
// 树的根节点
private transient Entry<K,V> root;
// 集合中元素个数
private transient int size = 0;
内部类
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
}
构造函数
// 无参构造
public TreeMap() {
// 使用无参构造,key必须实现Comparable接口(内部比较器)
comparator = null;
}
// 指定外部构造器
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
put()
public V put(K key, V value) {
Entry<K,V> t = root;
// 第一次put, t == null,新节点作为根节点,返回null
if (t == null) {
// 比较key,这里主要判断 key 是否使用了比较器
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
// 第n次put,t != null
int cmp;
Entry<K,V> parent;
// 1.根据比较器,选择父节点
Comparator<? super K> cpr = comparator;
if (cpr != null) {
// 1.1.使用外部比较器
do {
// 根节点出发,循环查找新节点的父节点
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
// 左节点作为父节点
t = t.left;
else if (cmp > 0)
// 右节点作为父节点
t = t.right;
else
// cmp == 0,替换value,并返回原value
return t.setValue(value);
} while (t != null);
}
else {
// 1.2.使用内部比较器
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left; // 左节点作为父节点
else if (cmp > 0)
t = t.right;// 右节点作为父节点
else
// cmp == 0, 替换value,并返回原value
return t.setValue(value);
} while (t != null);
}
// 2.构造新节点,并根据比较结果cmp,选择新节点成为父节点的左节点或右节点
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 3.为节点上色
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
- 第一次put,即root == null
- 调用
compare(),判断key是否使用比较器 - 数据封装成Entry节点,赋值给root,size = 1
- return null
- 调用
- 第n次put
- 根据比较器,从根节点开始遍历,调用
compare()的结果cmp比较key,选择合适的父节点parent;如果存在cmp为0,即key 相等,替换value,并返回oldValue - 创建新节点,根据cmp,选择新节点成为父节点parent的左节点或右节点
- 为节点上色
- 根据比较器,从根节点开始遍历,调用
本文详细介绍了Java中的Map接口,重点讲解了AbstractMap、HashMap和HashTable的实现原理。HashMap在JDK1.7和1.8的工作原理对比,包括负载因子、扩容策略和插入操作。同时,文章提到了HashTable的线程安全性以及TreeMap的升序特性。
798

被折叠的 条评论
为什么被折叠?



