在上一章中,我们深入了解了 HashMap
和 HashTable
的底层代码原理,包括它们的数据结构和工作原理。在本章中,我们将继续探讨 ConcurrentHashMap
、HashSet
和 LinkedHashMap
这些与 HashMap
有关的关键数据结构,深入了解它们的实现方式以及各自的特性和用途。让我们继续探索这些重要的 Java 集合类。如果您有任何特定的问题或需要更多详细信息,请随时提出。
ConcurrentHashMap
ConcurrentHashMap
是 Java 集合框架中的一个重要类,它提供了高度并发的哈希表实现。与普通的 HashMap
不同,ConcurrentHashMap
允许多个线程同时读取和写入,而不会导致数据不一致或死锁。它在多线程环境中提供了出色的性能和可伸缩性,并且在 Java 5 及以后的版本中引入。
以下是 ConcurrentHashMap
的一些关键特性和用途:
线程安全性:ConcurrentHashMap
是线程安全的数据结构。它通过使用分段锁机制来实现高度并发的读取和写入操作。这意味着多个线程可以同时读取不同部分的哈希表,而不会互相阻塞。这种设计使得在多线程环境中可以实现更高的性能。
分段锁:ConcurrentHashMap
内部使用多个独立的哈希表段(segment),每个段可以看作是一个小的哈希表。每个段都有自己的锁,当线程访问某一段时,只有该段被锁定,其他段仍然可以被并发访问。这种分段锁设计允许多个线程同时执行读操作,只有写操作需要锁定对应的段。
高性能:由于并发读取操作不会阻塞,ConcurrentHashMap
在读多写少的场景中表现出色。它是处理高并发读操作的理想选择。
支持高并发写操作:虽然 ConcurrentHashMap
的主要优势在于读操作的高并发性能,但它仍然支持高并发的写操作,因为不同的段可以被不同线程同时锁定。
可伸缩性:ConcurrentHashMap
具有良好的可伸缩性。您可以根据需要增加哈希表的段数,以适应更多的并发访问。这使得它适用于不同规模的应用。
迭代性能:ConcurrentHashMap
的迭代性能也得到了优化,使得在迭代哈希表中的元素时能够获得高效的性能。
高级功能:ConcurrentHashMap
提供了许多高级功能,如支持自定义并发级别、compute
方法用于原子更新、merge
方法用于原子合并等。
总之,ConcurrentHashMap
是一个强大的多线程环境中的哈希表实现,它允许高度并发的读取和写入操作,并具有出色的性能和可伸缩性。它通常用于需要高并发访问的场景,例如多线程应用程序中的缓存、计数器、任务分配等。
ConcurrentHashMap
以下是一个简单的示例代码,演示如何使用 ConcurrentHashMap
来管理多个线程安全的计数器。在这个示例中,我们创建一个 ConcurrentHashMap
,其中键是字符串,值是整数计数器。多个线程可以同时增加和获取计数器的值,而不会发生竞争条件。
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建一个 ConcurrentHashMap
ConcurrentHashMap<String, Integer> counterMap = new ConcurrentHashMap<>();
// 启动多个线程来增加计数器的值
for (int i = 0; i < 5; i++) {
String key = "counter" + i;
Thread thread = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
// 使用 compute 方法来原子地更新计数器
counterMap.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
}
});
thread.start();
}
// 等待所有线程完成
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印每个计数器的值
counterMap.forEach((key, value) -> {
System.out.println(key + ": " + value);
});
}
}
在这个示例中,我们创建了一个 ConcurrentHashMap
来存储多个计数器,每个计数器都有一个唯一的键(例如,“counter0”、“counter1” 等)。然后,我们启动了多个线程,每个线程对计数器进行增加操作。使用 compute
方法来原子地更新计数器的值,确保线程安全。最后,我们打印出每个计数器的最终值。
ConcurrentHashMap
在这种情况下非常适合,因为它允许多个线程同时访问和修改不同的计数器,而不会出现竞争条件。这是一个简单的示例,演示了如何在多线程环境中使用 ConcurrentHashMap
。
ConcurrentHashMap
底层代码解析ConcurrentHashMap
是 Java 集合框架中的一个关键类,它提供了高并发性能的哈希表实现。在内部,ConcurrentHashMap
使用了分段锁机制,允许多个线程同时读取和写入,而不会出现锁竞争的情况。
以下是 ConcurrentHashMap
的主要代码片段,我将添加注释来解释其关键部分:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
// 哈希表的默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 哈希表的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 段的数量,默认为 16
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 一个段的最小容量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
// 每个段内部存储键值对的数组
transient volatile Segment<K, V>[] segments;
// ...
// 内部类,表示哈希表的段
static final class Segment<K, V> extends ReentrantLock implements Serializable {
// ...
// 存储键值对的数组
transient volatile HashEntry<K, V>[] table;
// ...
// 内部类,表示哈希表的节点
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
final HashEntry<K, V> next;
// ...
}
// ...
}
// ...
// 内部方法,用于获取段的索引
final int segmentFor(int hash) {
return (hash >>> segmentShift) & segmentMask;
}
// 内部方法,用于插入键值对
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// ...
}
// ...
// 其他方法,如get, remove, size, clear, keySet, values, entrySet 等
}
上述代码摘录展示了 ConcurrentHashMap
的主要结构和关键部分。它使用了分段锁的思想,将整个哈希表划分为多个段(Segment
),每个段都是一个独立的哈希表,具有自己的锁。这意味着不同的线程可以同时访问不同的段,而不会相互阻塞。每个段内部使用数组来存储键值对,具有自己的哈希冲突解决机制。
ConcurrentHashMap
在并发读取和写入的场景中表现出色,因为不同的线程可以同时操作不同的段,而不会出现锁竞争。这使得它成为处理高并发情况下的理想选择。
是对其中一部分核心代码进行更详细的分析和讲解。
1. put
方法核心代码分析
put
方法用于向 ConcurrentHashMap
中插入键值对。以下是 put
方法的核心代码,我将逐行进行讲解和分析:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
Segment<K, V> s; // 获取要操作的段
if (value == null)
return null;
if ((s = segmentFor(hash)) == null)
throw new NullPointerException();
return s.put(key, hash, value, onlyIfAbsent); // 调用段的 put 方法
}
Segment
:首先声明了一个 Segment
对象 s
,用于表示要操作的段。
if (value == null) return null;
:检查传入的值是否为 null
,如果是 null
,则直接返回 null
。
if ((s = segmentFor(hash)) == null)
:通过 segmentFor
方法根据键的哈希值确定要操作的段,并将其赋值给 s
。如果 segmentFor
返回 null
,则抛出 NullPointerException
异常。
return s.put(key, hash, value, onlyIfAbsent);
:最后,调用确定的段 s
的 put
方法进行实际插入操作,传递键 key
、哈希值 hash
、值 value
和 onlyIfAbsent
参数。
2. get
方法核心代码分析
get
方法用于根据键获取值。以下是 get
方法的核心代码,我将逐行进行讲解和分析:
//会发现源码中没有一处加了锁
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算hash
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//读取首节点的Node元素
if ((eh = e.hash) == h) {
//如果该节点就是首节点就返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
//eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
//eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
//eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
int hash = hash(key.hashCode());
:首先,根据传入键的哈希码计算哈希值 hash
。
return segmentFor(hash).get(key, hash);
:然后,通过 segmentFor
方法根据哈希值确定要操作的段,然后调用该段的 get
方法,传递键 key
和哈希值 hash
来获取值。
3. remove
方法核心代码分析
remove
方法用于删除指定键的键值对。以下是 remove
方法的核心代码,我将逐行进行讲解和分析:
public V remove(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).remove(key, hash, null);
}
int hash = hash(key.hashCode());
:首先,根据传入键的哈希码计算哈希值 hash
。
return segmentFor(hash).remove(key, hash, null);
:然后,通过 segmentFor
方法根据哈希值确定要操作的段,然后调用该段的 remove
方法,传递键 key
、哈希值 hash
和一个 null
参数来删除键值对。
4. resize
方法核心代码分析
resize
方法用于扩容哈希表。以下是 resize
方法的核心代码,我将逐行进行讲解和分析:
final void reinitialize() {
if (table != null) {
for (int i = 0; i < segments.length; ++i)
segments[i].clear(); // 清空各个段
}
table = null;
sizeCtl = 0;
}
if (table != null)
:首先,检查哈希表是否已经创建(table
不为 null
)。
for (int i = 0; i < segments.length; ++i)
:然后,遍历各个段,执行清空操作。
segments[i].clear();
:通过调用 clear
方法清空各个段内的数据。
table = null;
:最后,将哈希表 table
置为 null
。
sizeCtl = 0;
:重置哈希表的控制变量 sizeCtl
。
这些是 ConcurrentHashMap
中的一些核心方法的关键代码分析。理解这些代码有助于了解 ConcurrentHashMap
的内部实现和线程安全机制。
ConcurrentHashMap
中的分段锁是通过将整个哈希表分成多个段(Segment)来实现的,每个段都拥有自己的锁。这样,多个线程可以同时访问不同段的数据,而不会相互阻塞,从而提高了并发性能。
下面是分段锁的原理分析和代码讲解:
原理分析:
分段:ConcurrentHashMap
通过将哈希表分成多个段来实现并发控制。每个段是一个独立的哈希表,拥有自己的锁。这样,不同段之间的操作可以并行进行,提高了并发性能。
锁粒度:每个段内部的锁粒度较小,仅锁定该段内的数据。这允许多个线程在不同段上并发执行操作,减小了锁的竞争。
分段数:分段的数量通常与 ConcurrentHashMap
的初始化容量相关,每个段负责管理一部分键值对。随着数据的增加,ConcurrentHashMap
可以动态地增加段的数量,以保持并发性能。
代码讲解:
以下是 ConcurrentHashMap
中分段锁的代码示例:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
// ...
static final class Segment<K, V> extends ReentrantLock implements Serializable {
// ...
// 存储键值对的数组
transient volatile HashEntry<K, V>[] table;
// ...
// 内部类,表示哈希表的节点
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
final HashEntry<K, V> next;
// ...
}
// ...
}
// ...
}
ConcurrentHashMap
中的每个段是 Segment
类的实例,它继承自 ReentrantLock
,表示一个可重入锁。
Segment
内部有一个存储键值对的数组 table
,用于存储数据。不同段拥有不同的 table
。
在每个段内部,有一个内部类 HashEntry
,表示哈希表的节点。键值对被存储在 HashEntry
中。哈希冲突时,会形成链表或红黑树结构。
Segment
内部的锁用于保护该段的数据结构,以确保多个线程同时访问该段时的线程安全性。
总结来说,ConcurrentHashMap
通过将哈希表分成多个段,并为每个段提供独立的锁,实现了分段锁机制。这种设计允许多个线程在不同段上并发执行操作,提高了并发性能,同时保持了线程安全。
ConcurrentHashMap
内部使用了哈希桶、链表或红黑树等数据结构来存储键值对,具体的数据结构选择取决于哈希冲突的情况。以下是这些数据结构的原理讲解和代码分析:
1. 哈希桶 (Hash Bucket) 原理:
哈希桶是一种数组结构,用于存储键值对。ConcurrentHashMap
将哈希表的每个段分成多个哈希桶,每个哈希桶负责存储一部分键值对。
哈希函数将键映射到特定的哈希桶,通常是通过取哈希码的某个部分再取模操作来实现的。
哈希冲突:当不同键映射到同一个哈希桶时,发生哈希冲突。此时,键值对将以链表的形式存储在该哈希桶中。
2. 链表 (Linked List) 原理:
链表是一种线性数据结构,用于存储键值对。当哈希冲突发生时,新的键值对将被追加到链表的末尾。
链表的查询操作需要顺序扫描链表,时间复杂度为 O(n),其中 n 是链表的长度。
3. 红黑树 (Red-Black Tree) 原理:
红黑树是一种自平衡二叉搜索树,用于存储键值对。当链表中的键值对数量达到一定阈值时,链表会被转换为红黑树,以提高查询效率。
红黑树的查询操作具有较低的时间复杂度 O(log n),其中 n 是树的高度。
下面是相关代码分析:
哈希桶的代码示例:
static final class Segment<K, V> extends ReentrantLock implements Serializable {
// 存储键值对的哈希桶
transient volatile HashEntry<K, V>[] table;
// ...
}
在每个 Segment
内部,有一个数组 table
用于存储键值对。不同的哈希桶可能有不同的 table
。
链表的代码示例:
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
final HashEntry<K, V> next;
// ...
}
在 HashEntry
中,next
字段用于链接下一个哈希冲突的键值对,从而形成链表结构。
红黑树的代码示例:
static final class TreeNode<K, V> extends Node<K, V> {
TreeNode<K, V> parent; // 父节点
TreeNode<K, V> left; // 左子节点
TreeNode<K, V> right; // 右子节点
TreeNode<K, V> prev; // 链表中的前一个节点
boolean red; // 是否为红色节点
// ...
}
在 ConcurrentHashMap
中,链表可以在键值对数量达到一定阈值后转换为红黑树。TreeNode
表示红黑树中的节点,其中的 parent
、left
、right
等字段用于表示树结构。
这些数据结构的选择和管理使 ConcurrentHashMap
能够在高并发场景中高效地存储和检索键值对,并且在哈希冲突较多时自动升级为红黑树以提高查询性能。这些结构的组合是 ConcurrentHashMap
实现高效并发操作的关键。
ConcurrentHashMap
和 HashMap
都是 Java 中用于存储键值对的集合类,但它们在并发性和线程安全性上有显著的区别。以下是它们的主要区别:
线程安全性:
ConcurrentHashMap
是线程安全的数据结构,它可以被多个线程同时访问和修改,而不需要额外的同步操作。它通过使用分段锁(Segment)来实现并发控制,每个段拥有自己的锁,不同段的操作可以并行执行。
HashMap
不是线程安全的,如果多个线程同时访问和修改一个 HashMap
实例,可能会导致数据不一致和线程安全问题。在多线程环境下,必须使用额外的同步机制(例如,Collections.synchronizedMap()
或显式的锁)来保证线程安全性。
性能:
HashMap
的性能通常比 ConcurrentHashMap
略好,因为不需要额外的并发控制开销。但在高并发环境中,ConcurrentHashMap
的性能通常更好,因为它可以允许多个线程同时读取和写入数据。迭代器的弱一致性:
ConcurrentHashMap
的迭代器提供了弱一致性的保证,即迭代器可以同时遍历正在修改的和未修改的元素,但不保证迭代器在某个特定时间点的视图是一致的。这对于并发迭代非常有用。
HashMap
的迭代器在并发环境中可能会导致 ConcurrentModificationException
异常,因为它不提供并发迭代的支持,必须在多线程环境中进行额外的同步。
初始容量和负载因子:
ConcurrentHashMap
在初始化时可以指定多个段(Segment),每个段内有自己的初始容量和负载因子。这允许更好地控制并发度和内存使用。
HashMap
则只有一个全局的初始容量和负载因子,不能针对不同需求进行调优。
null 值和键:
ConcurrentHashMap
不允许存储 null 值和 null 键。如果尝试存储 null 值或键,将会抛出 NullPointerException
。
HashMap
允许存储 null 值和键。
综上所述,ConcurrentHashMap
适用于高并发环境,提供了线程安全性和性能的平衡。而 HashMap
适用于单线程或低并发环境,需要额外的同步来确保线程安全。选择合适的集合类取决于应用程序的并发需求和性能要求。
HashSet
HashSet
是 Java 中的一个集合类,它是基于哈希表的数据结构实现的,用于存储一组唯一的对象。以下是关于 HashSet
的介绍:
唯一性:HashSet
中的元素是唯一的,不允许重复。如果尝试将重复元素添加到 HashSet
中,它将被忽略。
无序性:HashSet
中的元素没有特定的顺序,不保证元素的存储顺序和插入顺序相同。如果需要有序性,可以考虑使用 LinkedHashSet
。
基于哈希表:HashSet
的内部实现是基于哈希表,这使得查找元素的性能非常高效。平均情况下,插入、删除和查找操作的时间复杂度都是 O(1)。
线程不安全:HashSet
不是线程安全的集合,如果多个线程同时访问和修改一个 HashSet
实例,可能会导致并发问题。在多线程环境中,可以考虑使用 Collections.synchronizedSet()
或 ConcurrentHashSet
来确保线程安全性。
允许存储 null 元素:HashSet
允许存储一个 null 元素。
用途:HashSet
常用于需要存储一组元素并且需要快速查找元素的场景。它适用于大多数集合需求,例如去重、成员检查等。
需要注意的是,HashSet
不保证元素的顺序,因此遍历时元素的顺序可能不同于插入的顺序。如果需要有序性,可以考虑使用 LinkedHashSet
,它保留了元素的插入顺序。
HashSet
以下是一个示例代码,演示如何使用 HashSet
存储一组唯一的整数元素,并执行一些基本操作:
import java.util.HashSet;
public class HashSetExample {
public static void main(String[] args) {
// 创建一个 HashSet 实例,用于存储整数
HashSet<Integer> set = new HashSet<>();
// 添加元素
set.add(5);
set.add(10);
set.add(15);
set.add(20);
set.add(25);
// 尝试添加重复元素(不会成功,HashSet 不允许重复)
set.add(10);
// 查找元素
boolean contains15 = set.contains(15);
boolean contains30 = set.contains(30);
// 删除元素
set.remove(20);
// 遍历元素并打印
System.out.println("HashSet 中的元素:");
for (Integer num : set) {
System.out.println(num);
}
// 打印查找结果
System.out.println("HashSet 中是否包含 15:" + contains15); // true
System.out.println("HashSet 中是否包含 30:" + contains30); // false
}
}
在这个示例中,我们创建了一个 HashSet
实例 set
,并向其添加了一些整数元素。我们尝试添加了重复的元素 10
,但由于 HashSet
不允许重复,它只会保留一个 10
。我们还查找了元素 15
和 30
的存在,并删除了元素 20
。最后,我们遍历 HashSet
并打印其中的元素,以及查找结果。
HashSet
底层代码解析HashSet
的核心原理是基于哈希表实现的,它的内部使用哈希表来存储元素,并保持元素的唯一性。以下是 HashSet
的核心原理讲解和代码分析:
哈希表原理:
HashSet 的内部结构:
HashSet
内部使用了一个 HashMap
实例来存储元素。元素值作为键,而一个固定的占位对象作为值。这意味着 HashSet
实际上是一个键集合,值是固定的,不重要,只有键才具有意义。添加元素的过程:
HashSet
添加元素时,实际上是将元素值作为键,占位对象 PRESENT
作为值添加到内部的 HashMap
中。PRESENT
对象在 HashSet
内部是共享的,只有一个实例,用于节省内存。保持唯一性:
HashSet
通过哈希表来快速查找元素,确保元素的唯一性。在添加元素时,如果元素值已经存在于 HashSet
中,新的元素将不会被添加,从而保持了唯一性。以下是 HashSet
的关键代码示例:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
// 底层使用HashMap来保存HashSet中所有元素。
private transient HashMap<E,Object> map;
// 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。
private static final Object PRESENT = new Object();
/**
* 默认的无参构造器,构造一个空的HashSet。
*
* 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。
*/
public HashSet() {
map = new HashMap<E,Object>();
}
/**
* 构造一个包含指定collection中的元素的新set。
*
* 实际底层使用默认的加载因子0.75和足以包含指定
* collection中所有元素的初始容量来创建一个HashMap。
* @param c 其中的元素将存放在此set中的collection。
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
/**
* 以指定的initialCapacity和loadFactor构造一个空的HashSet。
*
* 实际底层以相应的参数构造一个空的HashMap。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<E,Object>(initialCapacity, loadFactor);
}
/**
* 以指定的initialCapacity构造一个空的HashSet。
*
* 实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。
* @param initialCapacity 初始容量。
*/
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity);
}
/**
* 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
* 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。
*
* 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
* @param dummy 标记。
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}
/**
* 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。
*
* 底层实际调用底层HashMap的keySet来返回所有的key。
* 可见HashSet中的元素,只是存放在了底层HashMap的key上,
* value使用一个static final的Object对象标识。
* @return 对此set中元素进行迭代的Iterator。
*/
public Iterator<E> iterator() {
return map.keySet().iterator();
}
/**
* 返回此set中的元素的数量(set的容量)。
*
* 底层实际调用HashMap的size()方法返回Entry的数量,就得到该Set中元素的个数。
* @return 此set中的元素的数量(set的容量)。
*/
public int size() {
return map.size();
}
/**
* 如果此set不包含任何元素,则返回true。
*
* 底层实际调用HashMap的isEmpty()判断该HashSet是否为空。
* @return 如果此set不包含任何元素,则返回true。
*/
public boolean isEmpty() {
return map.isEmpty();
}
/**
* 如果此set包含指定元素,则返回true。
* 更确切地讲,当且仅当此set包含一个满足(o==null ? e==null : o.equals(e))
* 的e元素时,返回true。
*
* 底层实际调用HashMap的containsKey判断是否包含指定key。
* @param o 在此set中的存在已得到测试的元素。
* @return 如果此set包含指定元素,则返回true。
*/
public boolean contains(Object o) {
return map.containsKey(o);
}
/**
* 如果此set中尚未包含指定元素,则添加指定元素。
* 更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2))
* 的元素e2,则向此set 添加指定的元素e。
* 如果此set已包含该元素,则该调用不更改set并返回false。
*
* 底层实际将将该元素作为key放入HashMap。
* 由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key
* 与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true),
* 新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变,
* 因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中,
* 原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。
* @param e 将添加到此set中的元素。
* @return 如果此set尚未包含指定元素,则返回true。
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
/**
* 如果指定元素存在于此set中,则将其移除。
* 更确切地讲,如果此set包含一个满足(o==null ? e==null : o.equals(e))的元素e,
* 则将其移除。如果此set已包含该元素,则返回true
* (或者:如果此set因调用而发生更改,则返回true)。(一旦调用返回,则此set不再包含该元素)。
*
* 底层实际调用HashMap的remove方法删除指定Entry。
* @param o 如果存在于此set中则需要将其移除的对象。
* @return 如果set包含指定元素,则返回true。
*/
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
/**
* 从此set中移除所有元素。此调用返回后,该set将为空。
*
* 底层实际调用HashMap的clear方法清空Entry中所有元素。
*/
public void clear() {
map.clear();
}
/**
* 返回此HashSet实例的浅表副本:并没有复制这些元素本身。
*
* 底层实际调用HashMap的clone()方法,获取HashMap的浅表副本,并设置到HashSet中。
*/
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
}
}
map
是 HashSet
内部用于存储元素的 HashMap
实例。
PRESENT
是一个占位对象,用于作为 HashMap
的值,不重要,只有键(元素值)才有意义。
add(E e)
方法用于向 HashSet
中添加元素。它实际上是通过调用 HashMap
的 put
方法来将元素值作为键,PRESENT
对象作为值存储到 map
中。如果添加成功,表示元素之前不在 HashSet
中,返回 true
;如果添加失败,表示元素已经存在,返回 false
。
总的来说,HashSet
利用了 HashMap
的高效性能和哈希表的特性,实现了高效的元素存储和查找,同时保持了元素的唯一性。这使得 HashSet
成为了一个常用的集合类,适用于需要存储一组唯一元素的情况。
在使用 HashSet
时,有一些注意事项需要考虑,以确保正确和高效地利用这个集合类:
唯一性:HashSet
中的元素是唯一的,不允许重复。要确保元素的唯一性,需要正确实现元素的 hashCode()
和 equals()
方法。如果不重写这两个方法,会使用默认的对象引用比较,导致无法识别相同内容的对象。
线程安全性:HashSet
不是线程安全的集合。在多线程环境下,如果多个线程同时访问和修改同一个 HashSet
实例,可能会导致并发问题。如果需要在多线程环境中使用,可以考虑使用 Collections.synchronizedSet()
或 ConcurrentHashSet
。
null 元素:HashSet
允许存储一个 null
元素。如果需要存储多个 null
元素,要格外小心,因为它们会被视为同一个元素。
元素顺序:HashSet
中的元素没有特定的顺序,不保证元素的存储顺序和插入顺序相同。如果需要有序性,可以考虑使用 LinkedHashSet
。
性能和容量:在创建 HashSet
实例时,可以指定初始容量和负载因子。合理选择这些参数可以提高性能。如果容量不足,会导致扩容操作,影响性能。
迭代器:HashSet
的迭代器提供了弱一致性的保证,允许在迭代过程中进行修改。但要注意,修改集合时需要小心,可能会导致 ConcurrentModificationException
异常。
哈希冲突:由于 HashSet
使用哈希表存储元素,可能会发生哈希冲突。良好的哈希函数和适当的容量选择可以减少冲突。
性能优化:在大规模数据集合中,为了提高性能,可以通过适当的容量、负载因子和哈希函数来优化 HashSet
的性能。
不可变对象:推荐使用不可变对象作为 HashSet
中的元素,以避免意外修改元素导致集合不稳定。
遍历元素:遍历 HashSet
中的元素时,可以使用增强型 for 循环或迭代器。注意,在迭代期间不要修改集合的内容,除非使用迭代器的 remove()
方法。
总之,了解这些注意事项有助于正确、高效地使用 HashSet
,并避免潜在的问题。根据具体的应用场景和需求,选择合适的集合类是非常重要的。
LinkedHashMap
LinkedHashMap
是 Java 中的一个集合类,它是 HashMap
的子类,提供了有序性的特性。与 HashMap
不同,LinkedHashMap
保持了元素的插入顺序或访问顺序,这使得你可以以特定顺序遍历其中的元素。以下是关于 LinkedHashMap
的介绍:
有序性:LinkedHashMap
保持了元素的有序性,这意味着你可以按照元素插入的顺序或访问的顺序(最近访问的元素排在前面)来遍历元素。有序性是通过一个双向链表来实现的。
基于哈希表:与 HashMap
相似,LinkedHashMap
也是基于哈希表来存储元素的。每个哈希桶(bucket)中存储的是一个链表,链表节点按照元素的插入或访问顺序连接起来。
线程不安全:与 HashMap
一样,LinkedHashMap
也不是线程安全的集合类。如果在多线程环境中访问和修改 LinkedHashMap
,可能需要进行额外的同步措施来确保线程安全性。
允许存储 null 元素:LinkedHashMap
允许存储一个 null
键和多个 null
值。
用途:LinkedHashMap
常用于需要按照插入顺序或访问顺序遍历元素的场景。它适用于构建缓存、LRU(Least Recently Used)算法等。
需要注意的是,LinkedHashMap
通常比 HashMap
稍微消耗更多的内存,因为它需要维护一个额外的链表结构来保持有序性。
LinkedHashMap
以下是一个示例代码,演示如何使用 LinkedHashMap
存储一组键值对,并按照插入顺序或访问顺序来遍历元素:
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapExample {
public static void main(String[] args) {
// 创建一个 LinkedHashSet 实例,按照插入顺序排序
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
// 添加元素
linkedHashMap.put("one", 1);
linkedHashMap.put("two", 2);
linkedHashMap.put("three", 3);
// 模拟访问元素
linkedHashMap.get("two");
// 遍历元素按照插入顺序或访问顺序
System.out.println("按照插入顺序或访问顺序遍历元素:");
for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
在这个示例中,我们创建了一个 LinkedHashMap
实例 linkedHashMap
,并向其中添加了一些键值对。我们还模拟了访问元素 "two"
,这会改变元素的顺序。最后,我们遍历 linkedHashMap
,观察到元素的顺序可以按照插入顺序或访问顺序来输出,具体取决于构造函数中的参数。
如果构造函数中传递了 true
,则按照访问顺序排序,最近访问的元素排在前面。如果构造函数中传递了 false
或不传递参数,则按照插入顺序排序。在示例中,我们传递了 true
,所以元素 "two"
被访问后排在第一位。
LinkedHashMap
底层代码解析LinkedHashMap
的核心代码分析和讲解涉及其内部数据结构和关键方法。以下是 LinkedHashMap
的核心代码分析和讲解:
数据结构:LinkedHashMap
使用了两个数据结构来维护元素的有序性:
table
):与 HashMap
相似,用于存储键值对,并用于快速查找元素。entrySet
):用于维护元素的顺序,链表中的每个节点包含一个键值对。构造函数:LinkedHashMap
有多个构造函数,但最常用的构造函数接受三个参数:初始容量(initialCapacity
)、负载因子(loadFactor
)和是否按照访问顺序排序(accessOrder
)。构造函数会根据传递的参数来初始化内部数据结构。
accessOrder
属性:LinkedHashMap
中有一个布尔属性 accessOrder
,用于表示是否按照访问顺序排序。如果 accessOrder
为 true
,则按照访问顺序排序;如果为 false
,则按照插入顺序排序。
get
方法:当调用 get
方法获取元素时,如果 accessOrder
为 true
,则该元素会被移到链表的尾部,表示最近访问的元素。
put
方法:当调用 put
方法添加元素时,元素会被添加到哈希表中,并根据 accessOrder
的值来决定是否将元素添加到链表的尾部。
remove
方法:当调用 remove
方法删除元素时,会同时从哈希表和链表中删除该元素。
afterNodeAccess
方法:该方法用于在访问元素后调整链表中的元素顺序,确保按照访问顺序排序。
afterNodeInsertion
方法:该方法用于在插入元素后调整链表中的元素顺序,确保按照插入顺序排序。
扩容机制:与 HashMap
类似,LinkedHashMap
也具有扩容机制,当元素数量达到容量乘以负载因子时,会触发扩容操作。
遍历元素:通过迭代器遍历 LinkedHashMap
时,可以按照插入顺序或访问顺序遍历元素,具体取决于 accessOrder
的值。
以下是一个简化的 LinkedHashMap
的源码示例,包含了关键逻辑的代码和注释:
import java.util.HashMap;
import java.util.Map;
public class MyLinkedHashMap<K, V> {
private static final int INITIAL_CAPACITY = 16;
private static final float LOAD_FACTOR = 0.75f;
private int capacity; // 哈希表容量
private Map.Entry<K, V>[] table; // 哈希表数组
private int size; // 元素数量
private final boolean accessOrder; // 是否按照访问顺序排序
private Map.Entry<K, V> head; // 链表头部
private Map.Entry<K, V> tail; // 链表尾部
public MyLinkedHashMap(boolean accessOrder) {
this.capacity = INITIAL_CAPACITY;
this.table = new Map.Entry[capacity];
this.accessOrder = accessOrder;
}
// 哈希函数,根据键计算哈希值
private int hash(Object key) {
return key.hashCode() % capacity;
}
// 获取键对应的哈希表索引
private int indexFor(int hash, int length) {
return hash & (length - 1);
}
// 获取键对应的节点
private Map.Entry<K, V> getNode(int hash, Object key) {
int index = indexFor(hash, table.length);
Map.Entry<K, V> e = table[index];
while (e != null && !(e.getKey() == key || e.getKey().equals(key))) {
e = e.getNext();
}
return e;
}
// 在链表头部添加节点
private void addFirst(Map.Entry<K, V> node) {
if (accessOrder) {
node.setNext(head);
head = node;
}
}
// 删除节点
private void removeNode(Map.Entry<K, V> node) {
if (accessOrder) {
Map.Entry<K, V> prev = head;
Map.Entry<K, V> current = head;
while (current != null) {
if (current == node) {
if (prev == current) {
head = current.getNext();
} else {
prev.setNext(current.getNext());
}
if (tail == current) {
tail = prev;
}
return;
}
prev = current;
current = current.getNext();
}
}
}
// 获取元素值
public V get(Object key) {
int hash = hash(key);
Map.Entry<K, V> node = getNode(hash, key);
if (node != null) {
if (accessOrder) {
removeNode(node); // 先移除再添加到头部,表示最近访问
addFirst(node);
}
return node.getValue();
}
return null;
}
// 添加元素
public void put(K key, V value) {
int hash = hash(key);
int index = indexFor(hash, table.length);
Map.Entry<K, V> node = table[index];
while (node != null) {
if (node.getKey() == key || node.getKey().equals(key)) {
node.setValue(value);
if (accessOrder) {
removeNode(node); // 先移除再添加到头部,表示最近访问
addFirst(node);
}
return;
}
node = node.getNext();
}
// 创建新节点并添加到哈希表
Map.Entry<K, V> newNode = new Node<>(hash, key, value, null);
newNode.setNext(table[index]);
table[index] = newNode;
size++;
if (accessOrder) {
addFirst(newNode);
}
// 判断是否需要进行扩容操作
if (size > capacity * LOAD_FACTOR) {
resize();
}
}
// 扩容操作
private void resize() {
// 省略扩容逻辑
}
// 其他方法和逻辑
private static class Node<K, V> implements Map.Entry<K, V> {
private final int hash;
private final K key;
private V value;
private Map.Entry<K, V> next;
public Node(int hash, K key, V value, Map.Entry<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public Map.Entry<K, V> getNext() {
return next;
}
public void setNext(Map.Entry<K, V> next) {
this.next = next;
}
}
// 其他内部类和方法
}
在 LinkedHashMap 的构造方法中,实际上调用了父类 HashMap 的相关构造方法来创建一个底层存储的 table
数组。
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
LinkedHashMap 的 Entry
元素继承自 HashMap 的 Entry
,并提供了双向链表的功能。在上述 HashMap 的构造器中,最后会调用 init()
方法,进行相关的初始化调用。尽管 init()
方法在 HashMap 的实现中并无实际意义,它仅提供给子类来实现相关的初始化调用。
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header;
}
LinkedHashMap 并未重写父类 HashMap 的 put
方法,而是重写了父类 HashMap 的 put
方法调用的子方法 void addEntry(int hash, K key, V value, int bucketIndex)
和 void createEntry(int hash, K key, V value, int bucketIndex)
,以提供自己特有的双向链接列表实现。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 调用 create 方法,将新元素以双向链表的形式加入到映射中。
createEntry(hash, key, value, bucketIndex);
// 删除最近最少使用元素的策略定义
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
if (size >= threshold)
resize(2 * table.length);
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
// 调用元素的 addBefore 方法,将元素加入到哈希、双向链接列表。
e.addBefore(header);
size++;
}
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
LinkedHashMap 重写了父类 HashMap 的 get
方法。在调用父类的 getEntry()
方法取得查找的元素后,它再判断排序模式 accessOrder
是否为 true
,如果是,则记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于链表的增加、删除操作是常量级的,因此不会带来性能损失。
public V get(Object key) {
// 调用父类 HashMap 的 getEntry() 方法,获取要查找的元素。
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
// 记录访问顺序。
e.recordAccess(this);
return e.value;
}
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
// 如果定义了 LinkedHashMap 的迭代顺序为访问顺序,
// 则删除以前位置上的元素,并将最新访问的元素添加到链表表头。
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
总之,LinkedHashMap
利用哈希表和双向链表的结合实现了有序性,可以根据需要选择按照插入顺序或访问顺序排序元素。这使得 LinkedHashMap
成为了一种常用的集合类,适用于需要保持元素顺序的场景,例如实现 LRU 缓存算法。核心代码分析有助于理解其工作原理和用法。
在使用 LinkedHashMap
时,有一些注意事项需要考虑:
有序性:LinkedHashMap
可以按照插入顺序或访问顺序排序元素。在创建 LinkedHashMap
实例时,根据需要选择合适的排序方式,使用构造函数中的 accessOrder
参数来指定。
性能:LinkedHashMap
的性能通常比 HashMap
稍差,因为它需要维护额外的链表结构。在大规模数据集合中,性能可能会受到影响,需要根据具体需求进行评估。
迭代顺序:如果使用迭代器遍历 LinkedHashMap
,注意遍历的顺序可能会影响性能。按照插入顺序遍历性能较好,按照访问顺序遍历会涉及链表操作。
线程安全性:LinkedHashMap
不是线程安全的,如果在多线程环境中使用,需要采取适当的同步措施,或者考虑使用 ConcurrentHashMap
。
内存消耗:由于 LinkedHashMap
需要维护额外的链表结构,可能占用更多的内存空间。在内存有限的情况下,要谨慎使用。
LRU 缓存:LinkedHashMap
的有序性使其非常适合实现LRU(最近最少使用)缓存策略。通过设置 accessOrder
为 true
,可以实现按照访问顺序排序的缓存。
元素的 equals
和 hashCode
方法:为了正确存储和检索元素,确保元素的 equals
和 hashCode
方法正确实现。否则,可能导致元素无法正确匹配。
键和值的类型:LinkedHashMap
可以存储任意类型的键和值,但要确保键的类型具有合适的 hashCode
和 equals
实现,以避免哈希冲突和不一致的行为。
容量和负载因子:可以根据数据集的大小和访问模式来调整初始容量和负载因子,以优化性能。默认值通常适用于多数情况。
总之,了解 LinkedHashMap
的特性和注意事项,根据具体需求和场景来选择合适的数据结构和配置参数,可以更好地利用它的功能。
当涉及到具体的应用场景时,这些集合类的应用会更加清晰。以下是一些示例,展示了这些集合类在不同应用场景中的用法:
秒杀活动(秒杀系统):
ConcurrentHashMap
存储商品库存信息,以便多个用户同时查询库存和进行秒杀。HashMap
存储用户订单信息,以便记录和查询用户的秒杀订单。HashSet
存储用户已秒杀商品的记录,用于去重,以确保用户不重复秒杀。LinkedHashMap
存储用户请求记录,以便按照请求顺序记录用户的请求,便于监控和分析。在线聊天应用:
HashMap
存储在线用户的信息,以便快速查找和管理在线用户。ConcurrentHashMap
存储聊天室的消息记录,支持多用户同时发送和接收消息。HashSet
存储用户的好友列表,用于快速查找和管理好友关系。LinkedHashMap
存储聊天消息的发送顺序,以便按照发送顺序显示消息。电子商务网站的购物车:
HashMap
存储用户购物车中的商品和数量。HashSet
存储用户喜欢的商品,以便用户添加到购物车或收藏夹。LinkedHashMap
存储用户的浏览记录,以便按照浏览顺序展示商品。数据统计应用:
HashMap
存储数据统计的结果,以便快速查找和查询统计数据。ConcurrentHashMap
存储多个线程并发更新的数据统计信息。LinkedHashMap
存储按照时间顺序记录的数据,以便生成时间序列图表。这些示例只是一些应用场景的代表性示例,实际应用中的用法会根据具体需求和业务逻辑的复杂性而变化。根据你的应用需求,可以选择合适的集合类来实现所需功能。
好的,本期我们的数组数据类型的讲解就到此结束。下期,我们将继续深入探讨链表数据结构,它将带领我们进入更加复杂和灵活的数据结构世界。如果您在学习过程中有任何问题或疑问,都欢迎在评论中提出或者私信联系我。我将竭尽全力为您提供更全面和详细的讲解,以确保您的学习得以顺利进行。期待下期的精彩内容!