java江湖系列——集合世家争霸(下)
文章目录
- 前言
- 一、集合世家起源
- 二、Set世家秘术
- 2.1 HashSet
- 2.1.1 HashSet秘籍
- 2.1.2 HashSet对敌策略
- 2.1.3 HashSet实战
- 2.2 LinkedHashSet
- 2.2.1 LinkedHashSet秘籍
- 2.2.2 LinkedHashSet对敌策略
- 2.2.3 LinkedHashSet实战
- 2.3 TreeSet
- 2.3.1 TreeSet秘籍
- 2.3.2 TreeSet对敌策略
- 2.3.3 TreeSet实战
- 三、List世家秘术
- 3.1 ArrayList
- 3.1.1 ArrayList秘籍
- 3.1.2 ArrayList对敌策略
- 3.1.3 ArrayList实战
- 3.2 LinkedList
- 3.2.1 LinkedList秘籍
- 3.2.2 LinkedList对敌策略
- 3.2.3 LinkedList实战
- 3.3 Vector
- 3.3.1 Vector秘籍
- 3.3.2 Vector对敌策略
- 3.3.3 Vector实战
- 3.4 Stack
- 3.4.1 Stack秘籍
- 3.4.2 Stack对敌策略
- 3.4.3 Stack实战
- 3.5 CopyOnWriteArrayList
- 3.5.1 CopyOnWriteArrayList秘籍
- 3.5.2 CopyOnWriteArrayList对敌策略
- 3.5.3 CopyOnWriteArrayList实战
- 四、Map世家秘术
- 4.1 HashMap
- 4.1.1 HashMap秘籍
- 4.1.2 HashMap对敌策略
- 4.1.3 HashMap实战
- 4.2 LinkedHashMap
- 4.2.1 LinkedHashMap秘籍
- 4.2.2 LinkedHashMap对敌策略
- 4.2.3 LinkedHashMap实战
- 4.3 TreeMap
- 4.3.1 TreeMap秘籍
- 4.3.2 TreeMap对敌策略
- 4.3.3 TreeMap实战
- 4.4 Hashtable
- 4.4.1 Hashtable秘籍
- 4.4.2 Hashtable对敌策略
- 4.4.3 Hashtable实战
- 4.4 ConcurrentHashMap
- 4.4.1 ConcurrentHashMap秘籍
- 4.4.2 ConcurrentHashMap对敌策略
- 4.4.3 ConcurrentHashMap实战
- 五、世家之争
- 献给读者
福利福利💌💌💌免费的JAVA学习资料网盘地址: 👉👉👉 点我!
福利福利💌💌💌免费的JAVA学习资料网盘地址: 👉👉👉 点我!
福利福利💌💌💌免费的JAVA学习资料网盘地址: 👉👉👉 点我!
前言
在这个数据驱动的时代,有效地组织、存储和操作数据是每个软件项目的核心需求。作为Java开发者,我们有幸拥有一个强大而灵活的工具——Java集合框架(Java Collections Framework
, JCF
)。自诞生以来,它已经成为构建高效、可维护代码的关键组件之一。无论是处理简单的数据列表,还是复杂的映射关系,Java集合框架都提供了丰富的接口、实现类和算法来帮助我们应对各种挑战。
然而,尽管Java
集合框架为我们的日常开发工作带来了极大的便利,但其丰富性和深度也意味着初学者可能会感到不知所措。不同的集合类型适用于不同类型的问题,选择合适的集合可以极大地影响程序的性能和易用性。因此,深入了解每个集合的特点、使用场景及其内部工作机制,对于想要编写高质量Java代码的开发者来说至关重要。
本博客旨在提供对Java
集合框架的一个全面概览,从基础概念到高级应用技巧,希望能为读者打下坚实的基础,并激发进一步探索的兴趣。无论你是刚刚接触Java的新手,还是希望深化理解经验丰富的开发者,这里都有适合你的内容。让我们一起开启这段学习之旅,探索Java
集合框架的无限可能吧!
继上一篇解释集合之后,本文将曝光集合世家所有的秘密,让集合世家透明的呈现给江湖系列读者。
一、集合世家起源
在编程的江湖里,集合就像是各个门派的秘籍,每个都有其独特的内功心法和招式。下面,就让我们用生动的江湖术语来揭开这些集合背后的底层实现原理。
1. Set(集合)—— 独行侠客
-
HashSet —— 风影步
绝技
:风影步,通过计算每个元素的哈希值(犹如每个独行侠客的独特标志),快速定位到数据的位置。特点
:无序且不允许重复,就像江湖上的独行侠客们,虽然各自行走江湖,但每个人都有自己独特的方式和领地,不会轻易与他人重合。弱点
:遇到两个或多个侠客的标志过于相似时(哈希冲突),他们就需要使用特殊的技巧(链地址法或开放地址法)来共处一地。
-
LinkedHashSet —— 连环锁
绝技
:连环锁,在保持了风影步的基础上,又增加了一条记忆之路(双向链表),使得每一位侠客不仅有自己的一席之地,还能记住进入江湖的先后顺序。特点
:有序且不允许重复,仿佛是结伴而行的侠客,虽各自独立却又相互关联,共同前行。
TreeSet —— 天梯阵绝技
:天梯阵,基于红黑树(平衡二叉搜索树)构建,将每位侠客按照实力(自然排序或自定义比较器)排列成一个有序的队伍。特点
:自动排序,确保队伍中的每一个成员都能找到自己的位置,无论是新来的还是老将,都能和谐相处。
2. List(列表)—— 江湖帮派
- ArrayList —— 流水阵
绝技
:流水阵,利用动态数组,让帮派成员能够迅速响应各种情况的变化,如同水流般灵活多变。特点
:允许重复,支持随机访问,就像帮派中的人可以自由进出,而且随时能找到任何一个成员的位置。但是,当人数过多时,可能需要扩建营地(扩容操作),这期间会稍微耽误一些时间。
- LinkedList —— 蛇形阵
绝技
:蛇形阵,以双向链表为基础,让帮派成员之间的联系更加紧密,头尾相连,前后呼应。特点
:插入和删除非常便捷,尤其是在队伍的两端。然而,想要找到队伍中间的某个人,则需要从头或尾开始一一询问,效率较低。
3. Map(映射)—— 密室藏宝图
- HashMap —— 幻影迷宫
绝技
:幻影迷宫,根据键的哈希值分配宝藏的位置,使寻宝者能够快速锁定目标。特点
:键值对存储,不保证顺序,高效查询。但如果迷宫设计不当(哈希冲突),则可能需要探索更长的路径才能找到宝藏。
- LinkedHashMap —— 追踪印记
绝技
:追踪印记,在幻影迷宫的基础上增加了足迹标记(双向链表),既能快速找到宝藏,也能知道宝藏被发现的先后顺序。特点
:既保留了快速查找的优点,又能按插入顺序或访问顺序维护元素,适合用于缓存等场景。
- TreeMap —— 山峰密道
绝技
:山峰密道,依据红黑树构造,所有宝藏都按照特定规则(自然排序或自定义比较器)隐藏在不同的高度上。特点
:自动排序,无论何时何地,只要遵循正确的路线,就能准确无误地找到所需的宝藏。
💡贴士:在这个充满挑战的编程江湖中,选择合适的“武功秘籍”(集合类型),对于每一位开发者来说都是至关重要的。希望这段江湖之旅能帮助你更好地理解集合的底层实现原理。
二、Set世家秘术
2.1 HashSet
2.1.1 HashSet秘籍
HashSet
实际上是通过 HashMap
来实现的。当你创建一个 HashSet
实例时,实际上是创建了一个 HashMap
实例来存储数据。每个 HashSet
中的元素都被视为 HashMap
中的键,而值则是一个常量对象。
元素的唯一性是通过 hashCode()
和 equals()
方法来保证的。当向 HashSet
添加元素时,首先会调用该元素的 hashCode()
方法来确定元素在哈希表中的位置。如果该位置上已经有其他元素,则会调用 equals()
方法比较这两个元素是否相同。如果相同,则不会添加新元素;如果不相同,则处理哈希冲突(例如链地址法
或开放地址法
)。
2.1.2 HashSet对敌策略
- 去重:当你有一个列表或者集合,想要去除其中的重复元素时,
HashSet
是非常方便的选择。 - 高效查找:对于需要频繁检查某个元素是否存在于集合中的情况,
HashSet
提供了高效的解决方案,因为它的查找操作时间复杂度接近O(1)
。 - 数学集合运算:包括交集、并集和差集等操作,虽然
HashSet
本身没有直接提供这些方法,但可以通过其API
轻松实现。
2.1.3 HashSet实战
import java.util.HashSet;
import java.util.Iterator;
public class HashSetExample {
public static void main(String[] args) {
// 创建一个HashSet实例
HashSet<String> hashSet = new HashSet<>();
// 1. 添加元素
System.out.println("添加元素:");
hashSet.add("Apple");
hashSet.add("Banana");
hashSet.add("Cherry");
hashSet.add("Apple"); // 尝试添加重复元素,不会生效
System.out.println("HashSet内容:" + hashSet);
// 2. 检查集合是否包含某个元素
System.out.println("\n检查元素是否存在:");
boolean containsApple = hashSet.contains("Apple");
System.out.println("HashSet是否包含'Apple':" + containsApple);
boolean containsOrange = hashSet.contains("Orange");
System.out.println("HashSet是否包含'Orange':" + containsOrange);
// 3. 删除元素
System.out.println("\n删除元素:");
boolean removed = hashSet.remove("Banana");
System.out.println("是否成功删除'Banana':" + removed);
System.out.println("删除后的HashSet内容:" + hashSet);
// 4. 遍历HashSet
System.out.println("\n遍历HashSet:");
// 使用增强型for循环
System.out.println("使用增强型for循环:");
for (String fruit : hashSet) {
System.out.println(fruit);
}
// 使用迭代器
System.out.println("\n使用迭代器:");
Iterator<String> iterator = hashSet.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
// 5. 判断集合是否为空
System.out.println("\n判断集合是否为空:");
boolean isEmpty = hashSet.isEmpty();
System.out.println("HashSet是否为空:" + isEmpty);
// 6. 获取集合的大小
System.out.println("\n获取集合的大小:");
int size = hashSet.size();
System.out.println("HashSet的大小:" + size);
// 7. 清空集合
System.out.println("\n清空集合:");
hashSet.clear();
System.out.println("清空后的HashSet内容:" + hashSet);
}
}
对敌结果
添加元素:
HashSet内容:[Apple, Cherry, Banana]
检查元素是否存在:
HashSet是否包含'Apple':true
HashSet是否包含'Orange':false
删除元素:
是否成功删除'Banana':true
删除后的HashSet内容:[Apple, Cherry]
遍历HashSet:
使用增强型for循环:
Apple
Cherry
使用迭代器:
Apple
Cherry
判断集合是否为空:
HashSet是否为空:false
获取集合的大小:
HashSet的大小:2
清空集合:
清空后的HashSet内容:[]
2.2 LinkedHashSet
2.2.1 LinkedHashSet秘籍
LinkedHashSet
实际上是通过HashMap
和双向链表结合的方式来实现的。每个LinkedHashSet
中的元素都被视为HashMap
中的键,而值则是一个常量对象。此外,它还使用了一个双向链表来记录元素的插入顺序。- 元素的唯一性同样通过
hashCode()
和equals()
方法来保证。当向LinkedHashSet
添加元素时,首先会调用该元素的hashCode()
方法来确定元素在哈希表中的位置。如果该位置上已经有其他元素,则会调用equals()
方法比较这两个元素是否相同。如果相同,则不会添加新元素;如果不相同,则处理哈希冲突。
2.2.2 LinkedHashSet对敌策略
- 去重且保持顺序:当你不仅需要去除重复项,还需要保持元素的插入顺序时,
LinkedHashSet
是非常合适的选择。例如,在构建一个需要去重但同时要保留用户输入顺序的系统时,LinkedHashSet
可以很好地满足需求。 - 高效查找:虽然
LinkedHashSet
主要用于保持插入顺序,但它依然提供了高效的查找能力,因为其底层基于哈希表实现。
2.2.3 LinkedHashSet实战
import java.util.LinkedHashSet;
public class LinkedHashSetExample {
public static void main(String[] args) {
// 创建一个LinkedHashSet实例
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
// 添加元素
linkedHashSet.add("Apple");
linkedHashSet.add("Banana");
linkedHashSet.add("Cherry");
linkedHashSet.add("Apple"); // 尝试添加重复元素,不会生效
// 输出LinkedHashSet内容
System.out.println("LinkedHashSet内容:" + linkedHashSet);
// 删除元素
boolean removed = linkedHashSet.remove("Banana");
System.out.println("是否成功删除'Banana':" + removed);
System.out.println("删除后的LinkedHashSet内容:" + linkedHashSet);
// 遍历LinkedHashSet
System.out.println("\n遍历LinkedHashSet:");
for (String fruit : linkedHashSet) {
System.out.println(fruit);
}
}
}
对敌结果
LinkedHashSet内容:[Apple, Banana, Cherry]
是否成功删除'Banana':true
删除后的LinkedHashSet内容:[Apple, Cherry]
遍历LinkedHashSet:
Apple
Cherry
2.3 TreeSet
2.3.1 TreeSet秘籍
TreeSet
底层依赖于TreeMap
实现,实际上是一个TreeMap
的键集合。每个元素作为TreeMap
的键,而值则是一个常量对象。- 红黑树是一种自平衡的二叉搜索树,具有以下特点:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点(null 节点)是黑色。
- 如果一个节点是红色,则其子节点必须是黑色(不能有两个连续的红色节点)。
- 从任意节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。
- 这些特性保证了红黑树的平衡性,从而确保插入、删除和查找操作的时间复杂度为
O(log n)
。
2.3.2 TreeSet对敌策略
- 自动排序
当需要存储一组数据并自动对其进行排序时,TreeSet
是非常好的选择。例如,存储分数、日期或姓名等需要排序的数据。 - 去重且排序
当需要同时去重和排序时,TreeSet
可以满足需求。例如,存储一批用户ID并按升序排列。 - 范围查询
TreeSet
支持高效的范围查询,例如获取某个区间内的所有元素。 - 最近邻查询
可以快速找到某个元素的前驱(小于当前元素的最大值)或后继(大于当前元素的最小值)。
2.3.3 TreeSet实战
import java.util.TreeSet;
public class TreeSetExample {
public static void main(String[] args) {
// 创建一个TreeSet实例(默认自然排序)
TreeSet<Integer> treeSet = new TreeSet<>();
// 添加元素
treeSet.add(10);
treeSet.add(5);
treeSet.add(20);
treeSet.add(15);
treeSet.add(5); // 尝试添加重复元素,不会生效
// 输出TreeSet内容
System.out.println("TreeSet内容:" + treeSet);
// 查找元素
System.out.println("\n查找元素:");
System.out.println("TreeSet是否包含5:" + treeSet.contains(5));
System.out.println("TreeSet是否包含8:" + treeSet.contains(8));
// 获取最大值和最小值
System.out.println("\n获取最大值和最小值:");
System.out.println("最小值:" + treeSet.first());
System.out.println("最大值:" + treeSet.last());
// 范围查询
System.out.println("\n范围查询:");
System.out.println("小于10的最大值:" + treeSet.lower(10));
System.out.println("大于10的最小值:" + treeSet.higher(10));
System.out.println("小于等于10的最大值:" + treeSet.floor(10));
System.out.println("大于等于10的最小值:" + treeSet.ceiling(10));
// 子集查询
System.out.println("\n子集查询:");
System.out.println("从5到15的子集(不含15):" + treeSet.subSet(5, 15));
System.out.println("小于15的子集:" + treeSet.headSet(15));
System.out.println("大于等于10的子集:" + treeSet.tailSet(10));
// 删除元素
System.out.println("\n删除元素:");
boolean removed = treeSet.remove(10);
System.out.println("是否成功删除10:" + removed);
System.out.println("删除后的TreeSet内容:" + treeSet);
}
}
对敌结果
TreeSet内容:[5, 10, 15, 20]
查找元素:
TreeSet是否包含5:true
TreeSet是否包含8:false
获取最大值和最小值:
最小值:5
最大值:20
范围查询:
小于10的最大值:5
大于10的最小值:15
小于等于10的最大值:10
大于等于10的最小值:10
子集查询:
从5到15的子集(不含15):[5, 10]
小于15的子集:[5, 10]
大于等于10的子集:[10, 15, 20]
删除元素:
是否成功删除10:true
删除后的TreeSet内容:[5, 15, 20]
三、List世家秘术
3.1 ArrayList
3.1.1 ArrayList秘籍
ArrayList
内部使用一个对象数组elementData
来存储元素。初始容量通常是 10,但可以通过构造函数指定不同的初始容量。- 当
ArrayList
达到其容量限制并尝试添加更多元素时,它会自动增加容量,通常是原容量的 1.5 倍左右。 - 由于基于数组实现,ArrayList 在尾部添加和访问元素都非常高效,但在头部或中间位置插入和删除元素效率较低,因为需要移动元素以维持连续性。
3.1.2 ArrayList对敌策略
- 频繁随机访问
如果你需要频繁地通过索引访问集合中的元素,ArrayList
是非常好的选择。 - 尾部操作为主
对于主要在列表尾部进行添加和删除操作的应用场景,ArrayList
提供了较好的性能表现。 - 需要维护插入顺序
当需要维护元素的插入顺序时,ArrayList
可以满足需求。
3.1.3 ArrayList实战
import java.util.ArrayList;
import java.util.Iterator;
public class ArrayListExample {
public static void main(String[] args) {
// 创建一个ArrayList实例
ArrayList<String> arrayList = new ArrayList<>();
// 添加元素
arrayList.add("Apple");
arrayList.add("Banana");
arrayList.add("Cherry");
// 输出ArrayList内容
System.out.println("ArrayList内容:" + arrayList);
// 获取特定位置的元素
String elementAt1 = arrayList.get(1); // 索引从0开始
System.out.println("\n索引1处的元素:" + elementAt1);
// 删除元素
boolean removed = arrayList.remove("Banana");
System.out.println("\n是否成功删除'Banana':" + removed);
System.out.println("删除后的ArrayList内容:" + arrayList);
// 遍历ArrayList
System.out.println("\n遍历ArrayList:");
// 使用增强型for循环
for (String fruit : arrayList) {
System.out.println(fruit);
}
// 使用迭代器
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
}
}
对敌结果
ArrayList内容:[Apple, Banana, Cherry]
索引1处的元素:Banana
是否成功删除'Banana':true
删除后的ArrayList内容:[Apple, Cherry]
遍历ArrayList:
Apple
Cherry
Apple
Cherry
3.2 LinkedList
3.2.1 LinkedList秘籍
LinkedList
的每个元素都被封装在一个内部类Node
中,该类包含了对前一个节点和后一个节点的引用。- 由于采用双向链表,
LinkedList
在添加或删除元素时只需修改相关节点的前后引用即可,无需像 ArrayList 那样复制大量数据。 - 但是,获取中间位置的元素需要从头或尾开始遍历整个链表,这会导致效率较低。
3.2.2 LinkedList对敌策略
- 频繁在两端操作
如果你需要频繁地在列表的头部或尾部进行添加和删除操作,LinkedList
提供了较高的性能。 - 实现栈或队列
由于实现了Deque
接口,LinkedList
可以非常方便地用来实现栈或队列的数据结构。 - 不需要随机访问
当你的应用主要涉及顺序访问元素,而不需要频繁通过索引访问元素时,LinkedList
是一个合适的选择。
3.2.3 LinkedList实战
import java.util.LinkedList;
import java.util.Iterator;
public class LinkedListExample {
public static void main(String[] args) {
// 创建一个LinkedList实例
LinkedList<String> linkedList = new LinkedList<>();
// 添加元素
linkedList.add("Apple");
linkedList.add("Banana");
linkedList.add("Cherry");
// 输出LinkedList内容
System.out.println("LinkedList内容:" + linkedList);
// 在头部添加元素
linkedList.addFirst("Orange");
System.out.println("\n在头部添加'Orange'后的LinkedList内容:" + linkedList);
// 在尾部添加元素
linkedList.addLast("Grape");
System.out.println("在尾部添加'Grape'后的LinkedList内容:" + linkedList);
// 删除第一个元素
String removedFirst = linkedList.removeFirst();
System.out.println("\n移除的第一个元素:" + removedFirst);
System.out.println("移除后的LinkedList内容:" + linkedList);
// 删除最后一个元素
String removedLast = linkedList.removeLast();
System.out.println("移除的最后一个元素:" + removedLast);
System.out.println("移除后的LinkedList内容:" + linkedList);
// 遍历LinkedList
System.out.println("\n遍历LinkedList:");
// 使用增强型for循环
for (String fruit : linkedList) {
System.out.println(fruit);
}
// 使用迭代器
Iterator<String> iterator = linkedList.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
}
}
对敌结果
LinkedList内容:[Apple, Banana, Cherry]
在头部添加'Orange'后的LinkedList内容:[Orange, Apple, Banana, Cherry]
在尾部添加'Grape'后的LinkedList内容:[Orange, Apple, Banana, Cherry, Grape]
移除的第一个元素:Orange
移除后的LinkedList内容:[Apple, Banana, Cherry, Grape]
移除的最后一个元素:Grape
移除后的LinkedList内容:[Apple, Banana, Cherry]
遍历LinkedList:
Apple
Banana
Cherry
Apple
Banana
Cherry
3.3 Vector
3.3.1 Vector秘籍
Vector
的内部实现与ArrayList
相似,主要区别在于所有的方法都加了synchronized
关键字来保证线程安全。- 当
Vector
达到其当前容量限制并尝试添加更多元素时,它会自动增加容量,默认情况下容量将翻倍。不过,可以通过构造函数或ensureCapacity(int minCapacity)
方法指定自定义的增长因子。
3.3.2 Vector对敌策略
- 需要线程安全的场合
如果你的应用需要在多线程环境下使用列表,并且对性能要求不是特别高,可以考虑使用Vector
。否则,推荐使用ArrayList
加上适当的同步机制,或者使用CopyOnWriteArrayList
等专门设计用于并发环境的集合类。 - 不频繁修改的列表
对于那些基本上只读或者很少写入的列表来说,Vector
提供了一种简单的方式来确保数据的一致性和完整性,而无需手动管理同步问题。
3.3.3 Vector实战
import java.util.Enumeration;
import java.util.Vector;
public class VectorExample {
public static void main(String[] args) {
// 创建一个Vector实例
Vector<String> vector = new Vector<>();
// 添加元素
vector.add("Apple");
vector.add("Banana");
vector.add("Cherry");
// 输出Vector内容
System.out.println("Vector内容:" + vector);
// 获取特定位置的元素
String elementAt1 = vector.get(1); // 索引从0开始
System.out.println("\n索引1处的元素:" + elementAt1);
// 删除元素
boolean removed = vector.remove("Banana");
System.out.println("\n是否成功删除'Banana':" + removed);
System.out.println("删除后的Vector内容:" + vector);
// 遍历Vector
System.out.println("\n遍历Vector:");
// 使用增强型for循环
for (String fruit : vector) {
System.out.println(fruit);
}
// 使用Enumeration遍历
Enumeration<String> enumeration = vector.elements();
while (enumeration.hasMoreElements()) {
String fruit = enumeration.nextElement();
System.out.println(fruit);
}
}
}
对敌结果
Vector内容:[Apple, Banana, Cherry]
索引1处的元素:Banana
是否成功删除'Banana':true
删除后的Vector内容:[Apple, Cherry]
遍历Vector:
Apple
Cherry
Apple
Cherry
3.4 Stack
3.4.1 Stack秘籍
- LIFO行为
栈是一种遵循后进先出原则的数据结构,即最后加入的元素最先被移除。 - 同步特性
由于Stack
继承自Vector
,所以它是线程安全的。但是,在大多数情况下,这种线程安全性并不是必要的,并且会导致性能上的开销。 - 动态数组底层实现
Stack 的底层实现基于Vector
,这意味着它可以动态调整大小以容纳更多的元素。 - 扩展功能
除了标准的栈操作(如push
和pop
),Stack
还提供了一些额外的方法,比如search
方法用于查找元素的位置等。
3.4.2 Stack对敌策略
- 性能考虑
虽然Stack
提供了线程安全的操作,但这通常意味着比非同步的替代方案(例如使用Deque
实现)更低的性能。如果你的应用不需要线程安全,考虑使用Deque
的实现来代替Stack
。 - 替代方案
在现代Java开发中,更推荐使用Deque
接口的实现类来代替Stack
。例如,ArrayDeque
和LinkedList
都可以作为栈使用,并且提供了更好的性能和灵活性。 - 历史背景
Stack
类是在Java
很早期就存在的,设计上有些过时。因此,在新项目中除非有特殊原因,否则应避免直接使用Stack
类。
3.4.3 Stack实战
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
// 创建一个Stack实例
Stack<String> stack = new Stack<>();
// 元素入栈
stack.push("Apple");
stack.push("Banana");
stack.push("Cherry");
// 输出栈内容
System.out.println("栈内容:" + stack);
// 查看栈顶元素
String topElement = stack.peek();
System.out.println("\n栈顶元素:" + topElement);
// 弹出栈顶元素
String poppedElement = stack.pop();
System.out.println("弹出的栈顶元素:" + poppedElement);
System.out.println("弹出后的栈内容:" + stack);
// 搜索元素的位置
int position = stack.search("Apple");
if (position != -1) {
System.out.println("\n'Apple' 在栈中的位置:" + position);
} else {
System.out.println("'Apple' 不在栈中。");
}
// 判断栈是否为空
boolean isEmpty = stack.empty();
System.out.println("栈是否为空:" + isEmpty);
}
}
对敌结果
栈内容:[Apple, Banana, Cherry]
栈顶元素:Cherry
弹出的栈顶元素:Cherry
弹出后的栈内容:[Apple, Banana]
'Apple' 在栈中的位置:2
栈是否为空:false
3.5 CopyOnWriteArrayList
3.5.1 CopyOnWriteArrayList秘籍
- 写时复制机制
- 每次对列表进行修改操作(如添加、删除或更新元素)时,都会创建一个新的数组,并将原有数组中的元素复制到新数组中,然后指向这个新数组。
- 原始数组保持不变,直到所有正在进行的读操作完成。
- 同步控制
- 所有的写操作都被同步保护,确保同一时刻只有一个线程可以修改列表。
- 读操作则完全不需要同步,因为它们总是访问最新的快照。
3.5.2 CopyOnWriteArrayList对敌策略
- 读多写少的场景
- 当你的应用中有大量的读操作,而写操作相对较少时,
CopyOnWriteArrayList
是一个不错的选择。例如:- 缓存系统中的数据存储。
- 事件监听器列表(如
GUI
应用程序中的事件处理器)。
- 弱一致性要求
如果你的应用可以接受迭代器返回的数据可能不是最新的版本,则可以考虑使用CopyOnWriteArrayList
。 - 避免读写冲突
在某些场景下,你希望读操作不受写操作的影响,比如统计数据的实时展示。
3.5.3 CopyOnWriteArrayList实战
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Iterator;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
// 创建一个CopyOnWriteArrayList实例
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 添加元素
list.add("Apple");
list.add("Banana");
list.add("Cherry");
// 输出列表内容
System.out.println("初始列表:" + list);
// 启动一个线程进行读操作
Thread readThread = new Thread(() -> {
Iterator<String> iterator = list.iterator();
System.out.println("\n读取线程开始:");
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
});
// 启动一个线程进行写操作
Thread writeThread = new Thread(() -> {
System.out.println("\n写入线程开始:");
list.add("Orange");
list.remove("Banana");
System.out.println("写入线程完成,列表:" + list);
});
// 启动线程
readThread.start();
writeThread.start();
try {
readThread.join();
writeThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 最终列表内容
System.out.println("\n最终列表:" + list);
}
}
对敌结果
初始列表:[Apple, Banana, Cherry]
写入线程开始:
写入线程完成,列表:[Apple, Cherry, Orange]
读取线程开始:
Apple
Cherry
Orange
最终列表:[Apple, Cherry, Orange]
四、Map世家秘术
4.1 HashMap
4.1.1 HashMap秘籍
💡贴士:关于
hashmap
的详解 👉👉👉点我!
- 哈希表
HashMap
使用哈希表作为底层数据结构。每个键通过其hashCode()
方法生成一个哈希码,然后根据这个哈希码决定该键值对存储在数组中的位置。 - 解决哈希冲突
当两个不同的键产生了相同的哈希码(哈希碰撞)时,Java 8 之前使用链地址法(将具有相同哈希值的元素链接成一个链表),而在 Java 8 及之后版本中,当链表长度超过一定阈值(默认为 8)时,会自动转换为红黑树以提高查询效率。 - 扩容机制
当哈希表中的元素数量超过了负载因子(默认为 0.75)乘以当前容量时,HashMap
会进行扩容,通常是将容量翻倍,并重新计算所有元素的位置(称为rehashing
)。
4.1.2 HashMap对敌策略
- 避免频繁的rehashing
如果预先知道要存储多少个元素,可以在创建HashMap
时指定初始容量,以减少扩容次数带来的性能开销。 - 线程安全问题
HashMap 不是线程安全的,在多线程环境中直接使用可能会导致数据不一致或其他并发问题。考虑使用Collections.synchronizedMap()
包装HashMap
或者直接使用ConcurrentHashMap
。 - 选择合适的负载因子
负载因子决定了哈希表何时进行扩容,默认值是0.75
,可以根据实际需求调整这个值来平衡空间利用率和性能。
4.1.3 HashMap实战
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// 创建一个HashMap实例
HashMap<String, Integer> hashMap = new HashMap<>();
// 添加键值对
hashMap.put("Apple", 1);
hashMap.put("Banana", 2);
hashMap.put("Cherry", 3);
// 输出HashMap内容
System.out.println("HashMap内容:" + hashMap);
// 获取特定键对应的值
int appleCount = hashMap.get("Apple");
System.out.println("\n'Apple'的数量:" + appleCount);
// 更新已有键的值
hashMap.put("Apple", 5);
System.out.println("更新后的HashMap内容:" + hashMap);
// 删除键值对
hashMap.remove("Banana");
System.out.println("删除'Banana'后的HashMap内容:" + hashMap);
// 判断是否包含某个键
boolean containsApple = hashMap.containsKey("Apple");
System.out.println("\nHashMap是否包含'Apple':" + containsApple);
// 获取HashMap大小
int size = hashMap.size();
System.out.println("HashMap的大小:" + size);
// 清空HashMap
hashMap.clear();
System.out.println("清空后的HashMap内容:" + hashMap);
}
}
对敌结果
HashMap内容:{Apple=1, Cherry=3, Banana=2}
'Apple'的数量:1
更新后的HashMap内容:{Apple=5, Cherry=3, Banana=2}
删除'Banana'后的HashMap内容:{Apple=5, Cherry=3}
HashMap是否包含'Apple':true
HashMap的大小:2
清空后的HashMap内容:{}
4.2 LinkedHashMap
4.2.1 LinkedHashMap秘籍
LinkedHashMap
继承自HashMap
,并在此基础上添加了一个双向链表来维护元素的顺序(插入顺序或访问顺序)。- 每个节点不仅包含哈希桶中的链接,还包含前驱和后继指针,用于维护链表结构。
根据构造函数的不同,可以选择是否启用按访问顺序排序。如果启用了按访问顺序排序,每次访问元素时(通过get
或put
方法),该元素将被移动到链表的末尾。
4.2.2 LinkedHashMap对敌策略
- 选择合适的排序模式
根据你的需求选择插入顺序还是访问顺序。对于一般的键值对存储需求,插入顺序足够;而对于缓存应用,通常需要启用访问顺序。 - 性能考虑
虽然LinkedHashMap
提供了额外的顺序管理功能,但它也因此带来了少量的性能开销。确保这种开销在你的应用场景中是可以接受的。 - 线程安全问题
LinkedHashMap
不是线程安全的,在多线程环境下直接使用可能会导致数据不一致或其他并发问题。考虑使用Collections.synchronizedMap()
包装LinkedHashMap
或者直接使用ConcurrentHashMap
。
💡贴士:关于
ConcurrentHashMap
的详解 👉👉👉点我!
4.2.3 LinkedHashMap实战
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapExample {
public static void main(String[] args) {
// 创建一个LinkedHashMap实例,启用按访问顺序排序
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
return size() > 3; // 设置最大容量为3
}
};
// 添加键值对
linkedHashMap.put("Apple", 1);
linkedHashMap.put("Banana", 2);
linkedHashMap.put("Cherry", 3);
System.out.println("初始LinkedHashMap:" + linkedHashMap);
// 访问某个键,会将其移到链表末尾
linkedHashMap.get("Apple");
System.out.println("访问'Apple'后的LinkedHashMap:" + linkedHashMap);
// 添加新键值对,超过最大容量,触发移除最旧条目
linkedHashMap.put("Date", 4);
System.out.println("添加'Date'后的LinkedHashMap:" + linkedHashMap);
}
}
对敌结果
TreeMap内容:{1=Cherry, 2=Banana, 3=Apple}
最小键:1, 对应值:Cherry
最大键:3, 对应值:Apple
比2大的最小键:3, 对应值:Apple
比2小的最大键:1, 对应值:Cherry
删除键2后的TreeMap内容:{1=Cherry, 3=Apple}
4.3 TreeMap
4.3.1 TreeMap秘籍
- 红黑树
TreeMap
使用红黑树作为其底层的数据结构,这是一种自我平衡的二叉搜索树。它确保了树的高度保持在log(n)
级别,从而保证了基本操作的效率。 - 节点结构
每个节点包含三个引用:左子节点、右子节点和父节点,以及键、值和颜色(用于保持树的平衡)。
4.3.2 TreeMap对敌策略
- 选择合适的排序方式
根据你的需求选择自然排序还是提供自定义的Comparator
。如果你的键类型没有实现Comparable
接口,则必须提供Comparator
。 - 性能考虑
虽然TreeMap
提供了有序遍历的能力,但由于其基于红黑树的实现,基本操作的时间复杂度为O(log n)
,比HashMap
的O(1)
更高。因此,在不需要有序遍历时,优先考虑使用HashMap
。 - 线程安全问题
TreeMap
不是线程安全的,在多线程环境下直接使用可能会导致数据不一致或其他并发问题。考虑使用Collections.synchronizedMap()
包装TreeMap
或者直接使用ConcurrentSkipListMap
。
4.3.3 TreeMap实战
import java.util.Map;
import java.util.TreeMap;
public class TreeMapExample {
public static void main(String[] args) {
// 创建一个TreeMap实例,默认按自然顺序排序
TreeMap<Integer, String> treeMap = new TreeMap<>();
// 添加键值对
treeMap.put(3, "Apple");
treeMap.put(2, "Banana");
treeMap.put(1, "Cherry");
// 输出TreeMap内容
System.out.println("TreeMap内容:" + treeMap);
// 获取最小键和最大键
int firstKey = treeMap.firstKey();
int lastKey = treeMap.lastKey();
System.out.println("\n最小键:" + firstKey + ", 对应值:" + treeMap.get(firstKey));
System.out.println("最大键:" + lastKey + ", 对应值:" + treeMap.get(lastKey));
// 获取比某个键大的最小键
Integer higherKey = treeMap.higherKey(2);
if (higherKey != null) {
System.out.println("\n比2大的最小键:" + higherKey + ", 对应值:" + treeMap.get(higherKey));
}
// 获取比某个键小的最大键
Integer lowerKey = treeMap.lowerKey(2);
if (lowerKey != null) {
System.out.println("比2小的最大键:" + lowerKey + ", 对应值:" + treeMap.get(lowerKey));
}
// 删除键值对
treeMap.remove(2);
System.out.println("\n删除键2后的TreeMap内容:" + treeMap);
}
}
对敌结果
TreeMap内容:{1=Cherry, 2=Banana, 3=Apple}
最小键:1, 对应值:Cherry
最大键:3, 对应值:Apple
比2大的最小键:3, 对应值:Apple
比2小的最大键:1, 对应值:Cherry
删除键2后的TreeMap内容:{1=Cherry, 3=Apple}
4.4 Hashtable
4.4.1 Hashtable秘籍
Hashtable
的底层实现基于哈希表,类似于HashMap
,但它通过在每个方法前加上synchronized
关键字来保证线程安全。- 当
Hashtable
达到其当前容量限制并尝试添加更多元素时,它会自动增加容量,默认情况下扩容因子为0.75
。
4.4.2 Hashtable对敌策略
- 性能问题
虽然Hashtable
提供了线程安全的操作,但在大多数情况下,它的性能不如HashMap
。对于大多数应用场景,尤其是在单线程环境中,更推荐使用HashMap
并根据需要自行处理同步问题。 - 替代方案
在现代Java开发中,更推荐使用ConcurrentHashMap
来代替Hashtable
。ConcurrentHashMap
提供了更好的并发性能,并且支持更高的灵活性和效率。 - 历史背景
Hashtable
是在 Java 很早期就存在的,设计上有些过时。因此,在新项目中除非有特殊原因,否则应避免直接使用Hashtable
类。
4.4.3 Hashtable实战
import java.util.Hashtable;
public class HashtableExample {
public static void main(String[] args) {
// 创建一个Hashtable实例
Hashtable<String, String> hashtable = new Hashtable<>();
// 添加键值对
hashtable.put("fruit1", "Apple");
hashtable.put("fruit2", "Banana");
hashtable.put("fruit3", "Cherry");
// 输出Hashtable内容
System.out.println("Hashtable内容:" + hashtable);
// 获取特定键对应的值
String fruitValue = hashtable.get("fruit2");
System.out.println("\n'fruit2'对应的值:" + fruitValue);
// 删除键值对
hashtable.remove("fruit2");
System.out.println("删除'fruit2'后的Hashtable内容:" + hashtable);
// 尝试插入null键或值将抛出NullPointerException
try {
hashtable.put(null, "Grape");
} catch (NullPointerException e) {
System.out.println("\n尝试插入null键或值导致异常:" + e.getMessage());
}
}
}
对敌结果
Hashtable内容:{fruit3=Cherry, fruit2=Banana, fruit1=Apple}
'fruit2'对应的值:Banana
删除'fruit2'后的Hashtable内容:{fruit3=Cherry, fruit1=Apple}
尝试插入null键或值导致异常:null
4.4 ConcurrentHashMap
4.4.1 ConcurrentHashMap秘籍
💡贴士:关于
ConcurrentHashMap
的详解👉👉👉点我!
Java 7 及之前
在 Java 7 中,ConcurrentHashMap
使用了 分段锁(Segment Locking
) 的机制:
ConcurrentHashMap
内部被划分为多个段(Segment
),每个段相当于一个小的哈希表。- 每个段都有自己的锁,不同段之间的操作互不影响,从而实现了更高的并发性能。
- 默认情况下,
ConcurrentHashMap
被分成 16 个段,这意味着理论上最多可以有 16 个线程同时写入不同的段。
Java 8 及之后
从 Java 8 开始,ConcurrentHashMap
进行了重构,去掉了分段锁机制,改用更高效的 CAS 操作 和 synchronized 锁:
- 基于
HashMap
的桶数组和链表/红黑树结构。 - 对于写操作,使用
synchronized
锁住具体的桶,而不是整个段。 - 对于读操作,完全无锁,直接访问数据。
- 当链表长度超过一定阈值(默认为 8)时,链表会转换为红黑树,以提高查询效率。
这种改进使得 ConcurrentHashMap
在 Java 8 中具有更好的性能和扩展性。
4.4.2 ConcurrentHashMap对敌策略
- 避免频繁的扩容
扩容会导致性能下降,因此可以根据预期的数据量初始化合适的容量,以减少扩容次数。 - 弱一致性迭代器
迭代器不会反映最新的修改操作,可能会看到部分更新或旧的数据。 - 不适用于所有场景
如果你的应用需要强一致性(例如事务性操作),ConcurrentHashMap
可能不适合,因为它的设计目标是高并发和弱一致性。 - Java 8 改进
如果你使用的是 Java 8 或更高版本,推荐充分利用computeIfAbsent
、merge
等高级方法,它们能够简化复杂的并发逻辑。
4.4.3 ConcurrentHashMap实战
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建一个ConcurrentHashMap实例
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
// 添加键值对
concurrentHashMap.put("Apple", 1);
concurrentHashMap.put("Banana", 2);
concurrentHashMap.put("Cherry", 3);
// 输出ConcurrentHashMap内容
System.out.println("初始ConcurrentHashMap:" + concurrentHashMap);
// 使用putIfAbsent方法
concurrentHashMap.putIfAbsent("Apple", 5); // 不会覆盖已有的值
concurrentHashMap.putIfAbsent("Date", 4); // 插入新的键值对
System.out.println("\n使用putIfAbsent后的ConcurrentHashMap:" + concurrentHashMap);
// 使用computeIfAbsent方法
concurrentHashMap.computeIfAbsent("Elderberry", k -> 6); // 插入新的键值对
System.out.println("使用computeIfAbsent后的ConcurrentHashMap:" + concurrentHashMap);
// 使用forEach遍历
System.out.println("\n使用forEach遍历ConcurrentHashMap:");
concurrentHashMap.forEach((key, value) -> {
System.out.println(key + " -> " + value);
});
// 删除键值对
concurrentHashMap.remove("Banana");
System.out.println("\n删除'Banana'后的ConcurrentHashMap:" + concurrentHashMap);
}
}
对敌结果
初始ConcurrentHashMap:{Apple=1, Cherry=3, Banana=2}
使用putIfAbsent后的ConcurrentHashMap:{Apple=1, Cherry=3, Date=4, Banana=2}
使用computeIfAbsent后的ConcurrentHashMap:{Apple=1, Cherry=3, Date=4, Elderberry=6, Banana=2}
使用forEach遍历ConcurrentHashMap:
Apple -> 1
Cherry -> 3
Date -> 4
Elderberry -> 6
Banana -> 2
删除'Banana'后的ConcurrentHashMap:{Apple=1, Cherry=3, Date=4, Elderberry=6}
五、世家之争
在江湖中,各门各派各有绝技,犹如编程世界中的 Set、List 和 Map,它们各有所长,却也需相辅相成,方能在武林大会(项目开发)上大放异彩。
List 好比是少林长拳,讲究的是顺序
与招式的连贯性
。它按部就班地记录着每一位弟子(元素)的入门顺序,无论是寻找特定的师兄师弟(通过索引访问),还是新收徒弟(添加元素),都能有条不紊。
Set 则似武当太极,强调的是独特性
和不可重复性
。在这片天地里,不容许有任何两个完全相同的招式存在,每一个动作都独一无二,确保了武学的纯粹与精深,正如集合中每个元素都是独一无二的存在。
Map 犹如丐帮打狗棒法,每招每式皆有其独特的对应关系
,一招一狗(键值对),精准无比。它不仅记住了招式(键),还能找到对应的破解之法(值),使得面对千变万化的敌人时,总能找到应对之策。
然而,真正的武林高手懂得融会贯通,将长拳的刚猛、太极的柔韧以及打狗棒法的机巧融为一体。同理,在编写代码时,我们也应善用 List 的有序性来维护数据的先后次序,利用 Set 来保证数据的独特性避免重复,借助 Map 来建立复杂而精准的数据关联。三者齐备,方能在这个信息飞速流转的时代,立于不败之地,成就一番大事业。如此这般,方显英雄本色,游刃有余于数字江湖之中。
献给读者
💯 计算机技术的世界浩瀚无垠,充满了无限的可能性和挑战,它不仅是代码与算法的交织,更是梦想与现实的桥梁。无论前方的道路多么崎岖不平,希望你始终能保持那份初心,专注于技术的探索与创新,用每一次的努力和进步书写属于自己的辉煌篇章。
🏰在这个快速发展的数字时代,愿我们都能成为推动科技前行的中坚力量,不忘为何出发,牢记心中那份对技术执着追求的热情。继续前行吧,未来属于那些为之努力奋斗的人们。
亲,码字不易,动动小手,欢迎 点赞 ➕ 收藏,如 🈶 问题请留言(评论),博主看见后一定及时给您答复,💌💌💌