关注“Java后端技术全栈”
回复“面试”获取全套面试资料
Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。从概念上而言,您可以将 List 看作是具有数值键的 Map。而实际上,除了 List 和 Map 都在定义 java.util 中外,两者并没有直接的联系。
Java 核心类中有很多预定义的 Map 类。在介绍具体实现之前,我们先介绍一下 Map 接口本身,以便了解所有实现的共同点。Map 接口定义了四种类型的方法,每个 Map 都包含这些方法。
覆盖的方法
我们将这 Object 的这两个方法覆盖,以正确比较 Map 对象的等价性。
更新方法
可以更改 Map 内容。
返回视图的 Map 方法
使用这些方法返回的对象,您可以遍历 Map 的元素,还可以删除 Map 中的元素。
访问方法
这些方法检索有关 Map 内容的信息但不更改 Map 内容。
Java 自带了各种 Map 类。这些 Map 类可归为三种类型:
在工作中常用的Map实现类有HashMap、Hashtable(基本上也不用了)、ConcurrentHashMap。
下面来对这三个进行一个总结。
HashMap 是以key-value 键值对形式存储数据,允许 key 为 null(多个则覆盖),也允许 value 为 null。底层结构是数组 + 链表 + 红黑树。
主要属性:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
//存储元素的数组
transient Node<K,V>[] table;
//存放元素的个数,非Node数组长度
transient int size;
//记录结构性修改次数,用于快速失败
transient int modCount;
//阈值
int threshold;
//负载因子,默认0.75,用于扩容
final float loadFactor;
/**
* 静态内部类,存储数据的节点
*/
static class Node<K,V> implements Map.Entry<K,V> {
//节点的hash值
final int hash;
//节点的key值
final K key;
//节点的value值
V value;
//下一个节点的引用
Node<K,V> next;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
}
数据结构:数组 + 单链表,Node 结构:hash|key|value|next
HashMap数据结构
特征:
使用场景:
哈希冲突的解决方案:
put() 存储的流程(Java 8):
resize() 扩容的流程(Java 8):
扩容过程比较复杂, 迁移算法与 Java 7 不一样,Java 8 不需要每个元素都重新计算 hash,迁移过程中元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置。
get() 查询的流程(Java 8):
get() 注意事项:Java 8 没有把 key 为 null 放到数组 table[0] 中。
remove() 删除的流程(Java 8):
remove() 注意事项:删除单个 key,注意返回是的键值对中的 value。
为什么使用位运算(&)来代替取模运算(%):
HashMap 在 Java 7 和 Java 8 中的区别:
和 HashMap 一样,Hashtable 也是一个哈希散列表,Hashtable 继承于 Dictionary,使用重入锁 Synchronized 实现线程安全,key 和 value 都不允许为 Null。HashTable 已被高性能的 ConcurrentHashMap 代替。
主要属性:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
//真正存储数据的数组
private transient Entry<?,?>[] table;
//存放元素的个数,非Entry数组长度
private transient int count;
//阈值
private int threshold;
//负载因子,默认0.75
private float loadFactor;
//记录结构性修改次数,用于快速失败
private transient int modCount = 0;
/**
* 静态内部类,存储数据的节点
*/
private static class Entry<K,V> implements Map.Entry<K,V> {
//节点的hash值
final int hash;
//节点的key值
final K key;
//节点的value值
V value;
//下一个节点的引用
Entry<K,V> next;
}
//使用同步锁修饰的方法
public synchronized int size() {
return count;
}
public synchronized boolean isEmpty() {
return count == 0;
}
public synchronized V put(K key, V value) {
//...
}
public synchronized boolean equals(Object o) {
//....
}
}
快速失败原理是在并发场景下进行遍历操作时,如果有另外一个线程对它执行了写操作,此时迭代器可以发现并抛出 ConcurrentModificationException,而不需等到遍历完后才报异常。
数据结构:链表的数组,数组 + 链表,Entry 结构:hash|key|value|next
特征:
原理:
与 HashMap 不一样的流程是定位数组下标逻辑,HashTable 是在 key.hashcode() 后使用取模,HashMap 是位运算。HashTable 是 put() 之前进行判断是否扩容 resize(),而 HashMap 是 put() 之后扩容。
ConcurrentHashMap 在 Java 8 版本中丢弃了 Segment(分锁段)、ReentrantLock、HashEntry 数组这些概念,而是采用CAS + Synchronized 实现锁操作,Node 改名为 HashEntry,引入了红黑树保证查询效率,底层数据结构由数组 + 链表 + 红黑树组成,Node 数组默认为 16。数据结构如下图:
ConcurrentHashMap数据结构
数据结构(Java 8):Node[] 数组 + 单链表 + 红黑树
put() 存储的流程(Java 8):
resize() 扩容的流程(Java 8):
扩容的原理是创建新的数组,长度是原来的两倍,然后把旧数组数据迁移到新的数组中,在多线程情况下,需要注意线程安全问题,在解决安全问题的同时,还需要关注其效率。
get() 查询的流程(Java 8):
get() 注意事项:
整个过程都不需要加锁,因为读取数据的属性使用 volatile 修饰,实现线程可见性。
remove() 删除的流程(Java 8):
remove() 注意事项:
remove 函数底层是调用 replaceNode 函数实现结点的删除。
使用场景:并发、线程不安全场景
TreeMap 实现了 SotredMap 接口,意味着可以排序,是一个有序的集合。底层数据结构是红黑树结构,TreeMap 中的每个元素都存放在红黑树的节点上,默认使用自然排序,也可以自定排序,线程不安全。
主要属性:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
//排序比较器
private final Comparator<? super K> comparator;
//红黑树根节点
private transient Entry<K,V> root;
//集合长度
private transient int size = 0;
//记录结构性修改次数,用于快速失败
private transient int modCount = 0;
//红黑树常量
private static final boolean RED = false;
private static final boolean BLACK = true;
/**
* 实际存储数据的节点
*/
static final class Entry<K,V> implements Map.Entry<K,V> {
//节点的key值
K key;
//节点的value值
V value;
//左子树引用
Entry<K,V> left;
//右子树引用
Entry<K,V> right;
//父节点引用
Entry<K,V> parent;
//节点颜色(默认黑色)
boolean color = BLACK;
}
}
数据结构:红黑树(高效的检索二叉树),Entry 结构:key|value|left|right|parent|color
特征:
put() 存储的流程:
主要分为两个步骤,构建排序二叉树和构建平衡二叉树。
构建排序二叉树,过程如下:
举个栗子:put(9),步骤如下:
在这里插入图片描述
构建平衡二叉树,经过左旋、右旋、着色这些步骤后,得到一棵平衡二叉树。
remove() 的流程:比 put() 复杂,也是分两个步骤,删除节点和着色旋转。
删除节点,删除时出现以下 3 种情况:
着色旋转,进行颜色的对调和旋转,达到红黑树的特征。
LinkedHashMap 是使用 HashMap 机制实现。LinkedHashMap 在 HashMap 的基础上增加 before 和 after 两个属性来保证了迭代顺序。迭代顺序可以是插入顺序(默认),也可以是访问顺序。线程不安全。
主要属性:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
/**
* 静态内部类,存储数据的节点
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
//头结点
transient LinkedHashMap.Entry<K,V> head;
//尾结点
transient LinkedHashMap.Entry<K,V> tail;
//排序标识,true访问顺序迭代;false插入顺序迭代(默认)
final boolean accessOrder;
}
数据结构:数组 + 双向链表,Entry 结构:before|hash|key|value|next|after,before 和 after 用于维护整个双向链表。
特征:
原理:
LinkedHashMap 继承了 HashMap,hash 算法也是跟 HashMap 一致。LinkedHashMap 在 Entry 中新增了 before 和 after 两个属性来维护双向链表的迭代顺序。Entry 的 next 属性是维护 Entry 连接顺序,而 after 是维护迭代顺序。LinkedHashMap 使用 LRU 算法实现访问顺序排序,比如 get() 操作后的元素会移动到链表末尾,从而实现按访问顺序迭代。
使用场景:保证插入和访问顺序