前言
俗话说: “巧妇难为无米之炊。”作为一名开发者,大部分场景下均需要用容器来存储和处理数据,选择可用的容器很容易,但是恰如其分的容器是有点困难的,如何才可以快速选择合适且高效的容器呢 ?这需要小伙伴们对于容器特点、底层原理、应用场景有深入了解,接下来让我们一起探索容器的世界吧,紧跟我的脚步,相信你一定会收获满满。
一、 集合容器的前世今生
问 : " 圣僧,你从哪里来?又将到哪里去?"
答 : " 贫僧,自东土大唐而来,去往西天取经去。路过贵宝地 ... "
同理可得,学习任何一个新的技术,要学习的点有 : 产生的背景是什么? 解决了什么问题 ? 有哪些优势?
预知详情,请看下文讲解:
Java早期版本(1.0)中,开发者需要自行管理对象的存储和检索,可采用数组或自定义类两种方式。数组是一种固定长度的数据结构,一旦创建,其大小就不能改变、只可通过索引访问元素、不支持泛型。(核心问题 : 数组长度如何取值? 过大可能会产生空间冗余,过小需要进行多次扩容和数据搬移); 开发者还可以通过创建自定义类来实现特定的数据存储需求,自定义类中会提供添加、删除、访问元素的方法,虽然灵活但是需要处理很多底层细节,如内存管理、类型安全等。 鉴于以上的这些令人头痛的问题,容器应运而生,它的出现将数据的存储和处理从 石器时代 升级到 蒸汽时代。
Java容器最早由 Java Collections Framework(JCF)在Java 2版本被引入。由于其具有 灵活性、高效性、可扩展性、泛型支持以及降低开发成本等优点被开发者欣然接受。
1、灵活性 : 提供了多种接口和实现类,支持存储不同的类型的数据,满足各种数据处理需求。与固定长度数组相比,
集合容器可以根据需要动态地增加或减少元素,提供灵活性。
2、高效性 : 集合容器提供高效数据访问和操作方法,包括添加、删除等。例如: ArrayList支持随机访问、LinkedList
在插入和删除元素时表现出色。
3、可扩展性 : 容器可以自动扩容来适应数据处理需求,无需像数组一样预先定义长度,提供了很好的扩展性。
4、泛型支持 : 可在集合中定义元素类型,增强代码安全性,避免ClassCastException错误,提高可读性和可维护性。
5、降低开发成本 :直接拿来主义,不用从头开始写集合类,节省开发时间,提高效率。
小提示:
引入了容器并不代表就可以利用好容器哦,要想融会贯通,不仅要知其然,还要知其所以然。
二、细谈容器核心知识
图一 集合容器核心关系图(粗略)
图二 集合容器核心关系图(稍详细)
由图一所示,集合容器主要是由两大接口派生而来: Collection接口 和 Map接口
- Collection接口 : 存放单一类型元素,其下三个核心子接口有 List、Set、Queue 。
- Map接口 : 存放键值对元素(key底层使用Set)。
详细的结构如图二所示。
1、Collection 核心接口
接口 | 特点 | 常见应用场景 |
List | 存储有序,可重复的元素。(排序小能手) | 排序、搜索 ... |
Set | 存储无序,不可重复的元素。(独一无二) | 数据去重、多集合交、并、差集 ... |
Queue | 按指定规则确定先后顺序(存储有序、可重复元素) | 线程池、消息队列 ... |
1.1 List
图三 List接口实现类关系图
List接口实现类大 PK :
实现类 | 线程安全性 | 实现方式 | 默认初始容量 | 扩容倍数 | 使用场景 |
Vector | 安全(synchronized) | 数组 | 10 | 1 | 多读少写 |
Stack | 安全(synchronized) | 数组 | 10 | 1 | 多读少写 |
ArrayList | 不安全 | 数组 | 0 | 1.5 | 多读少写 |
LinkedList | 不安全 | 双向链表 | - | - | 多写少读 |
ArrayList优势 : ① 将很多数组操作的细节封装起来。 ② 支持动态扩容。
1.2 Set
图四 Set接口实现类关系图
Set接口实现类大PK:
实现类 | 线程安全性 | 实现方式 | 是否允许null | 适用场景 | ||
TreeSet | 不安全 | 红黑树 | 不允许 | 排序定制(自然排序 | 自定义比较器) | ||
HashSet | 不安全 | 哈希表 | 不允许 | 快速插入和查找(不关心顺序) | ||
LinkedHashSet | 不安全 | 哈希表+链表 | 不允许 | 保持元素插入顺序 |
补充说明:
· HashSet: 底层结构是哈希表,元素无序且不允许重复。插入和查询(包括contains方法)时间复杂度是O(1) 。· TreeSet: 底层结构是红黑树,元素按照自然顺序排序,不允许插入null元素。插入、删除和查找时间复杂度都O(logN)。
· LinkedHashSet: 底层数据结构是链表和哈希表,元素按照插入顺序排序,不允许重复也不允许为null 。
1.3 Queue
来源 : 网络转载
根据其特性和用途分类如下:
- 普通队列(LinkedList、PriorityQueue -- 支持优先级排序,无界)
- 双端队列(ArrayDeque、LinkedList -->支持在两端插入和移除元素)
- 阻塞队列 (LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue)
- 无界队列(LinkedList、ArrayDeque --> 无固定容量限制)
- 优先队列(PriorityQueue 、PriorityBlockingQueue --> 内部元素有序)
- 延迟队列 (DelayQueue -- 插入元素时可指定延迟时间,延迟时间过后,元素会变得可见<场景: 定时任务或事件调度>)
- 同步队列 (SynchronousQueue -- 用于线程之间直接传递元素,无内部缓冲区,即传即用)
2、Map 接口
图五 Map接口实现类关系图
实现类 | 底层结构 | null ? | 是否安全 | 使用场景 | |
HashMap | 数组 + 链表+红黑树 | 均允许 | 否 | 频繁插入、删除、查找操作 | |
TreeMap | 红黑树 | 均不允许 | 否 | 需排序的键的映射 | |
LinkedHashMap | 数组 + 链表+红黑树 | 均允许 | 否 | 保持排序的映射 | |
Hashtable | 数组 + 链表+红黑树 | 均不允许 | 是(synchronzied) | 多线程环境 | |
Properties | 数组+ 链表+红黑树 | 均不允许 | 是(synchronzied) | 配置文件管理、持久化 |
说明: 多线程环境下推荐使用 Collections.synchronziedMap 而非Hashtable 。
三、 容器常用方法集汇
1、Collection 接口
方法 | 作用 |
add(E obj) | 新增集合元素 |
addAll(Collection e) | 新增集合到新的集合中 |
boolean remove(Object obj) | 移除集合元素 |
boolean removeAll(Collection e) | 移除集合(所有元素) |
boolean removeIf(Predicate filter) | 移除符合条件的元素 |
boolean retainAll(Collection e) | 保留两个元素的交集 |
boolean isEmpty() | 判断当前集合是否为空集合 |
boolean contains(Object obj) | 判断当前集合中是否包含 obj 对象的元素 |
boolean containsAll(Collection<?> c) | 判断集合c是否是当前集合的 “子集” |
int size() | 获取当前集合中实际存储的元素个数 |
Object[] toArray() | 返回包含当前集合中所有元素的数组 |
1.1 List
(1)ArrayList
方法 | 作用 |
void add(E ele) | 新增集合元素 |
void add(index index,E ele) | 集合指定位置添加元素 |
void addAll(int index,Collection ele) | 集合指定位置添加元素集合 |
E get(int index) | 获取某一个位置的元素对象 |
List subList (int fromIndex,int toIndex) | 获取指定区间内的子集合 |
int indexOf(Object obj) | 获取元素首次出现位置索引 |
int lastIndexOf(Object obj) | 获取元素最后一次出现位置索引 |
E remove(int index) | 移除指定索引位置的元素 |
E set(int index,E ele) | 给指定索引位置重新赋值 |
(2)LinkedList
方法 | 作用 |
void add(E ele) | 将元素添加到链表末尾 |
void add(index index,E ele) | 指定位置添加元素 |
remove(Object obj) | 移除链表中的元素 |
remove(int index) | 移除指定位置的元素 |
E get(int index) | 获取某一个位置的元素对象 |
E geFirst() | 获取链表的第一个元素 |
E getLast() | 获取链表的最后一个元素 |
isEmpty() | 判断链表是否为空 |
contains(Object o) | 判断链表是否存在元素 |
size() | 获取链表的个数 |
addFirst(E e) | 将元素添加到链表头部 |
addLast(E e) | 将元素添加到链表尾部(与add方法相同) |
poll() | 移除并返回链表的第一个元素 |
pollFirst() | 移除并返回链表的第一个元素 |
pollLast() | 移除并返回链表的最后一个元素 |
push(E e) | 将元素推入链表(实际上是调用addFirst) |
pop() | 从链表中弹出第一个元素(实际上是调用 removeFirst) |
1.2 Set
(1)TreeSet
方法 | 作用 |
void add(E ele) | 将元素e添加到TreeSet中,若TreeSet之前不包含e,返回 true |
clear() | 移除TreeSet中的所有元素 |
clone() | 返回TreeSet的浅表副本 |
comparator() | 返回用于排序TreeSet中元素的比较器,若使用自然排序,返回null |
contains(Object o) | 若TreeSet集合中包含指定元素,则返回true |
first() | 返回TreeSet中第一个元素 |
last() | 返回TreeSet最后一个元素 |
headSet(E toElement) | 返回此Set的子集,包含从SortedSet中去除toElement的元素 |
subSet(E fromElement,E toElement) | 返回Set子集,从fromElement(包含)到toElement(不包含)的元素 |
tailSet(E fromElement) | 返回Set子集,包含从SortedSet中去除或保留fromElement的元素 |
remove(Object o) | 若TreeSet包含指定的元素,则将其移除 |
size() | 返回TreeSet的大小(即TreeSet中的元素数) |
(2)HashSet
方法 | 作用 |
void add(E ele) | 若set中尚未存储指定元素,则添加此元素(可选操作) |
remove(Object obj) | 若set中存在指定元素,则移除此元素(可选操作) |
contains(Object o) | 若set集合包含指定元素,则返回true |
size() | 返回set中的元素数(其大小) |
isEmpty() | 若set集合中不包含元素,则返回true |
clear() | 移除set集合中所有元素 |
iterator() | 返回set中元素上进行迭代的迭代器 |
toArray() | 返回包含set所有元素的数组 |
hashCode() | 返回set的哈希码值 |
equals(Object o) | 若指定对象也是一个set,并且包含相同的元素,则返回true |
clone() | 返回set的浅表副本 |
addAll(Collection<? extends E> c) | 若set集合中无collection中所有元素,则将其添加到此set集合中 |
removeAll(Collection<?> c) | 移除set中那些包含在collection中的元素 |
retainAll(Collection<?> c) | 仅保留 set中那些包含在指定collection中的元素 |
containsAll(Collection<?> c) | 若此set包含指定collection所有元素,则返回true |
toString() | 返回set的字符串表示形式,包含set中的元素 |
(3)LinkedHashSet
方法 | 作用 |
boolean add(E e) | 若set集合中尚未存储指定元素,则添加此元素 |
void clear() | 移除此set中所有元素 |
boolean contains(Object o) | 如果此set包含指定元素,则返回true |
boolean is Empty() | 若此set不包含元素,则返回false |
int size() | 返回此set中的元素数(set容量) |
Object[] toArray() | 返回包含此set中所有元素的数组 |
<T> T[] toArray(T[] a) | 返回包含此set中所有元素的数组;返回数组的运行时类型指定数组的类型 |
boolean remove(Object o) | 若指定元素存在于此set中,则移除它 |
Iterator<E> iterator() | 返回此set中元素按照插入顺序进行迭代的迭代器 |
Spliterator<E> spliterator() | 返回在此set中的元素上按照插入顺序进行迭代的 Spliterator |
2、Map 接口
(1)HashMap
方法 | 作用 |
put(K key, V value) | 将键值对添加到HashMap中 |
get(Object key) | 返回指定键所映射的值;如果此映射不包含该键的映射,则返回null |
size() | 返回HashMap中键值对的数量 |
isEmpty() | 如果HashMap为空,返回true |
containsKey(Object key) | 若此映射包含指定键的映射,则返回 true |
containValue(Object value) | 若此映射将一个或多个键映射到指定值,则返回true |
remove(Object key) | 若存在一个键的映射,则移除它 |
clear() | 移除HashMap中所以键值对 |
keySet() | 返回HashMap中所有键的Set视图 |
values() | 返回HashMap中所有值的Collection视图 |
entrySet() | 返回HashMap中所有映射的Set视图 |
(2)LinkedHashMap
方法 | 作用 |
V get(Object key) | 返回指定键值对应的值 |
V put(K key,V value) | 将指定键值对添加到Map中 |
V remove(Object key) | 移除Map中指定键的映射 |
void clear() | 移除Map中所有映射 |
boolean contiansKey(Object key) | 若此映射包含指定键的映射,返回true |
boolean containsVlaue(Object value) | 若此映射将一个或多个键映射到指定值,则返回true |
int size() | 返回Map中键值对的数量 |
boolean isEmpty() | 若Map不包含键值对,则返回true |
boolean equals(Object o) | 比较指定对象与此LinkedHashMap是否相等 |
int hashCode() | 返回此 LinkedHashMap的哈希值 |
Set<K> keySet() | 返回Map中所有键的Set视图 |
Collection<V> values() | 返回Map中所有值的Collection视图 |
Set<Map.Entry<K,V>> entrySet() | 返回Map中所有映射的Set视图 |
(3)TreeMap
方法 | 作用 |
put( K key, V value) | 添加元素到集合 |
get(Object key) | 根据键值获取对应的值 |
remove(Object key) | 根据键值移除元素 |
containsKey(Object key) | 判断集合是否包含指定的键 |
keySet() | 获取所有的键的集合 |
values() | 获取所有值的集合 |
entrySet() | 获取所有键值对映射关系 |
size() | 获取集合的大小 |
firstKey() | 获取集合的第一个键 |
lastKey() | 获取集合的最后一个键 |
lowerKey(K key) | 获取集合中小于指定键的最大键 |
floorKey(K key) | 获取集合中小于指定键的最大键 |
ceilingKey(K key) | 获取集合中大于指定键的最小键 |
higherKey(K key) | 获取集合中大于指定键的最小键 |
pollFirstEntry() | 移除并返回集合中的第一个键值对 |
pollLastEntry() | 移除并返回集合中的最后一个键值对 |
三、 容器底层原理剖析
1、ArrayList
1.1 参数分析
/**
* 默认的初始化容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 静态常量,表示一个空的数组实例
* 作用如下:
* 1、 节省内存(当创建空的ArrayList时可直接使用,不需要再次创建新的空数组)
* 2、 避免重复创建(所有ArrayList共享这个空数组)
* 3、 简化初始化(初始为空时,可直接使用EMPTY_ELEMENTDATA来初始化)
* 场景: 空的ArrayList构造函数
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 静态常量,表示一个空的数组实例
* 场景: 用于带有初始容量的构造函数(初始化容量为0时使用)
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* elementData存储所有元素的容器
* transient作用:
* (1) 避免序列化数据(若是序列化数据可能会导致不必要的数据传输和存储开销)
* (2) 提高安全性 (若ArrayList包含敏感数据,不序列化可以提供安全性,防止数据泄露)
* (3) 简化序列化逻辑(elementData不会被自动序列化,可更灵活控制序列化内容)
* 序列化与反序列化
* · 序列化: ArrayList被序列化时,elementData不会被写入序列化文件(序列化逻辑会手动处理ArrayList元素)
* · 反序列化: ArrayList被反序列化时,elementData会被初始化为默认值(通常是null或空数组,反序列化逻辑会手动恢复elementData的内容)
*/
transient Object[] elementData;
/**
* 作用: 防止数组容量超出int类型最大值Integer.MAX_VALUE
* 为什么减去8 ?
* (1) 防止整数溢出
* · 数组容量接近Integer.MAX_VALUE时,若继续增加容量,可能会导致整数溢出
* · 减去8 是为了留出余地,确保即使在进行一些额外操作时候也不会发生溢出
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 容器中真实0存储元素的个数(而非容量大小)
*/
private int size;
1.2 构造器
/**
* 空参构造器
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造一个有一定容量大小的容器
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 新建大小为initialCapacity的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 非法参数
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 根据一个Collection创建一个新的ArrayList容器
*/
public ArrayList(Collection<? extends E> c) {
// 将入参集合转化为数组
Object[] a = c.toArray();
// 将数组 a 的长度赋值给 size 变量,并检查是否为零
if ((size = a.length) != 0) {
// c是ArrayList类型,直接赋值给elementData
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 非ArrayList类型,用Arrays.copyof方法复制数组a
// 好处: 确保ArrayList持有的是独立数组副本,而不是原始集合的引用
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 使用空数组替代
elementData = EMPTY_ELEMENTDATA;
}
}
1.3 核心方法
add(E e) - 新增元素
源码分析如下:
public boolean add(E e) {
// 确保容量是否足够
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ensureCapacityInternal(int minCapacity) - 确保容量是否够
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
calculateCapacity(Object[] elementData, int minCapacity)
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判断容器是否为空,若为空直接返回默认容量值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 容器非空直接返回传入的参数值
return minCapacity;
}
ensureExplicitCapacity(int minCapacity)
private void ensureExplicitCapacity(int minCapacity) {
// 内部计数器,记录结构修改次数(多线程环境中检测并发修改,避免数据不一致的问题)
modCount++;
// 判断容量是否足够(条件成立表示不够进行扩容)
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
grow(int minCapacity)
private void grow(int minCapacity) {
// 获取当前elementData的长度
int oldCapacity = elementData.length;
// 计算新容量(当前容量的1.5倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若新容量小于minCapacity
if (newCapacity - minCapacity < 0)
// 将新容量设置为 minCapacity
newCapacity = minCapacity;
// 新容量超过 MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 调用hugeCapacity方法确定新容量
newCapacity = hugeCapacity(minCapacity);
// 将原数组复制到新数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
hugeCapacity(int minCapacity)
private static int hugeCapacity(int minCapacity) {
// 若是最小容量小于0,直接抛出异常
if (minCapacity < 0)
throw new OutOfMemoryError();
// 返回新容量
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
说明 :并发修改检测: modCount 用于检测 ArrayList 在迭代过程中是否有并发修改。若迭代过程中发生了修改,modCount会增加,从而触发 ConcurrentModificationException 异常。
相关文章: ArrayList扩容原理-腾讯云开发者社区-腾讯云
add(int index,E element)
public void add(int index, E element) {
rangeCheckForAdd(index);
// 判断容量大小并根据情况来决定是否扩容
ensureCapacityInternal(size + 1);
// 将从索引 index开始到数组末尾的所有元素向后移动
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
indexOf(Object obj) | lastIndexOf(Object obj)
// 查询第一次出现某个元素的索引位置
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
// 查询最后一次出现某个元素的索引位置
public int lastIndexOf(Object o) {
if (o == null) {
// 对于空值要做特殊处理
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
remove(int index)
public E remove(int index) {
// 检查索引范围
rangeCheck(index);
modCount++;
// 获取索引位置元素
E oldValue = elementData(index);
// 判断是否是中间或开始位置,若是的话,就将索引位置以后的所有元素向前移动覆盖
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;
return oldValue;
}
sort(Comparator<? super E> c )
@Override
@SuppressWarnings("unchecked")
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
// 通过工具类采用指定的比较器对对象进行排序
Arrays.sort((E[]) elementData, 0, size, c);
// 检查在修改时是否出现了版本变更
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
2、LinkedList
2.1 参数分析
/**
* 链表元素个数
*/
transient int size = 0;
/**
* 指向第一个节点的指针
*/
transient Node<E> first;
/**
* 指向最后一个节点的指针
*/
transient Node<E> last;
// 链表节点
private static class Node<E> {
// 节点内容
E item;
// 前驱节点指针
Node<E> next;
// 后驱节点指针
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
2.2 构造器
/**
* 空参构造器
*/
public LinkedList() {
}
/**
* 构造一个包含特定集合的一个列表,并根据这个集合的迭代器封装返回结果
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
2.3 核心方法
linkFirst(E e)
功能: 链接传入元素作为链表第一个或最后一个元素。(通过指针的修改)
/**
* 链接 e 作为链表的第一个元素
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
/**
* 链接 e 作为链表的最后一个节点
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
3、HashMap
底层数据结构: 数组 + 链表 + 红黑树 (JDK1.8) 数组+链表(JDK 1.7)
图六 HashMap底层存储结构图
如图六所示,jdk8 中的结构就是 数组 + 链表 + 红黑树 ,代码层面结构如下所示 :
//数组
transient Node<K,V>[] table;
//单链表节点
static class Node<K,V> implements Map.Entry<K,V> {
//hash值
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; }
//重写hashCode()方法(计算K-V节点hash值的另一种逻辑)
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//重写hashCode()方法,equals()也要重写
public final boolean equals(Object o) {
if (o == this)
return true;
//如果两个节点的value和key均相等,此两个键值对节点一定相同
return o instanceof Map.Entry<?, ?> e
&& Objects.equals(key, e.getKey())
&& Objects.equals(value, e.getValue());
}
}
//红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//当前节点的父节点
TreeNode<K,V> parent; // red-black tree links
//左孩子
TreeNode<K,V> left;
//右孩子
TreeNode<K,V> right;
//前驱节点
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//此处代码略
}
3.1 参数分析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
@java.io.Serial
private static final long serialVersionUID = 362498820763181265L;
//默认数组的初始化容量(只能以2的指数倍进行扩容操作)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//数组最大容量 2的30次方(1,073,741,824)
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶的树化阈值,如果一个桶(单链表)中的节点数量大于该值,需要将桶转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶的反树化阈值,如果一个树中的节点个数小于改值,需要将该桶从红黑树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
//控制树化或扩容
//如果集合中K-V键值对节点数大于该值并且某个桶中的键值对数大于8,该桶才会进行树化操作
static final int MIN_TREEIFY_CAPACITY = 64;
//记录HashMap集合的数组
transient Node<K,V>[] table;
// 该Set集合存储了当前集合所有K-V键值对节点的引用
transient Set<Map.Entry<K,V>> entrySet;
//记录K-V键值对节点的数量
transient int size;
//记录初始化后写操作的次数
transient int modCount;
// table数组下一次扩容的门槛,其大小=当前集合容量*loadFactor
int threshold;
// 自定义的负载因子
final float loadFactor;
//代码省略
}
说明:
DEFAULT__LOAD_FACTOR :加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量,当哈希表中的条目量大于(容量*加载因子)时,会执行扩容操作,扩容为当前容量的2倍。加载因子若为1,虽然提高了空间利用率,但是查找的时间变长;加载因子为0.5(较小),虽然提升了查找效率,但是空间利用率降低。
简而言之,负载因子维护着集合存储所需空间资源和集合操作所需的时间资源之间的平衡。
补充: 如果对负载因子为什么是 0.75还有疑问的话可以看下如下两篇文章
1、 java-HashMap的负载因子为什么默认是0.75?(图文并茂)
2、 为什么 HashMap 的加载因子是0.75?(原理讲解)
3.2 构造器
//构造器一: 无参构造,设置集合的负载因子为默认值(在添加元素时会调用resize根据默认容量初始化table数组)
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//构造器二: 含参构造,指定初始化容量
public HashMap(int initialCapacity) {
this(initialCapacity, 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;
//isNaN:检查其参数是否是非数字值(返回值是boolean)
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor(initialCapacity)
功能: 返回一个比传入值大且接近2的幂数的值。
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//功能:返回传入参数最高位非0位前面0的个数(包括符号位在内)
public static int numberOfLeadingZeros(int i) {
// HD, Count leading 0's
if (i <= 0)
return i == 0 ? 32 : 0;
int n = 31;
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
return n - (i >>> 1);
}
原理分析:
-1 的二进制表达: 1111 1111 1111 1111 1111 1111 1111 1111 (补码表示)
无符号右移(>>>)29位后得到: 0000 0000 0000 0000 0000 0000 0000 0111 (十进制表示:7)
最终返回值 : 8
3.3 核心方法
Put ( key,Value)
存放数据流程如下:
源码分析如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断数组是否为空,如果为空,则调用resize()方法进行扩容
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;
// 判断key是否相等(key相等,hashCode一定相等,反之不一定--> 如果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循环,将新节点与链表中的节点一一比较
for (int binCount = 0; ; ++binCount) {
// 条件成立,表示到达了链表的尾节点
if ((e = p.next) == null) {
// 将新节点插入到链表尾部
p.next = newNode(hash, key, value, null);
// 条件成立,转化成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 取下一个节点
p = e;
}
}
// 条件成立,表示key相等,直接更新对应的value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
思考区
· 为什么通过与操作(&)可以生成一个合适的(不越界限、分布均匀-- 依靠hash算法)的数组下标呢?
假设:
经过resize()初始化,此时数组容量是16 ,在插入第一个元素时计算hashCode=0101 0110
说明:
① 经过运算,无论hash值有多大,经过与运算(&)所求出的范围在数组下标范围之内
② (n-1)很关键,高位均是0,低位均是1
· (!onlyIfAbsent || oldValue == null) 中的oldValue判断有什么作用?
下面来讲解: putIfabsent()方法
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
说明: 底层调用的仍然是putVal方法,但是传入的onlyIfAbsent值是true
如何使用?
若待存的元素的key已在数组中存在,查看数组中对应元素的value是否为null,如果为null,则覆盖,反之,则不覆盖。
public static void main(String[] args) {
HashMap<String, String> hashMap = new HashMap<>(16);
hashMap.put("k1",null);
hashMap.putIfAbsent("k1","k2");
System.out.println(hashMap.get("k1"));
hashMap.put("k3","k3");
hashMap.putIfAbsent("k3","k4");
System.out.println(hashMap.get("k3"));
}
运行结果:
Resize() -- 初始化或扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
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;
// 初始化数组(默认大小是16)
@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;
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;
}
TreeifyBin()
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果数组为空或长度小于64,去扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 转化为红黑树(具体实现如下)
else if ((e = tab[index = (n - 1) & hash]) != null) {
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)
hd.treeify(tab);
}
}
Treeify() -> 链表转化为红黑树
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
// 将树化后的红黑树放到数组中的位置
moveRootToFront(tab, root);
}
Resize() --> 扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果扩容前的数组容量达到了最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容量扩容为原来的二倍
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;
// 当前位置存储的节点是红黑树节点
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 当前位置存储的是节点个数大于等于2的一个链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 条件成立,存储在新数组的老下标位(判断高bit位是不是 0 )
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { // 条件成立,存放在新数组(老下标+oldCap)下标位置
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;
}
思考区
情况一: 数组当前遍历位置没有元素
情况二:数组当前遍历位置只有一个元素(Node)
处理过程: 直接将该位置的一个元素移动到新数组的相同位置。
情况三:数组当前遍历位置不止有一个元素(Node)
处理过程: 将原链表拆分成两个链表,一个放在新数组的低位,另一个放在新数组的高位
具体过程演示如下:
拆分成两个链表插入新数组中
情况四:数组当前遍历位置是红黑树
遍历红黑树的双向链表,拆分为高位和低位链表,并分别记录链表中的节点个数。
① 低位链表不为空 & 节点个数小于6
处理逻辑:将树节点退化为链表节点,最终退化为链表(树节点单向链表退化为普通节点单向链表)
② 低位链表不为空 & 节点个数大于等于6个 & 高位链表不为null
处理逻辑: 将低位链表树化为红黑树
说明: 如果高位链表为空,直接将原位置红黑树移到新数组位置即可(无需树化)
③ 高位链表不为空 & 节点个数小于6
处理逻辑: 将树节点退化为链表节点,最终退化为链表(树节点单向链表退化为普通节点单向链表)
④ 高位链表不为空 & 节点个数大于等于6 & 低位链表不为null
处理逻辑: 将低位链表转化为红黑树
说明:如果高位链表为空,直接将原位置红黑树移到新数组位置即可(无需树化)
Get(key)
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
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;
}