Java 进阶--集合:告别数组的“僵硬”,拥抱灵活的数据容器
作者:IvanCodes
发布时间:2025年5月1日🫡
专栏:Java教程
大家好!👋 还记得我们上次聊的数组 (Array) 吗?它很基础,性能也不错,但有个致命的缺点:长度一旦定了就不能改 <🔒>!想象一下,你用数组存购物车里的商品 🛒,刚开始定了装 10 件,结果顾客想买第 11 件… 咋办?重新创建一个更大的数组,再把旧的东西搬过去?太麻烦了!😫
为了解决数组这种“僵硬”的问题,以及提供更丰富的数据组织方式,Java 提供了一套强大的 集合框架 (Java Collections Framework, JCF)。你可以把它想象成一个超级工具箱 🧰,里面有各种大小可变、功能各异的容器 🧺,专门用来高效地存储和管理对象。今天,我们就来打开这个工具箱,看看里面有哪些宝贝!✨
一、为什么需要集合?它比数组强在哪? 🤔🚀
相比数组,集合主要有这些优势:
- 动态大小 <📏➡️<♾️>>:大部分集合的大小是可变的,你可以随时添加或删除元素,不用担心容量问题。
- 丰富的功能 <⚙️>: 集合框架提供了大量现成的方法来操作数据,比如添加、删除、查找、排序、迭代等,比数组方便得多。
- 不同的数据结构 <🗺️>: 集合框架提供了多种数据结构(如列表、集、队列、映射),每种都有不同的特点(如是否有序、是否允许重复),可以根据需求选择最合适的。
- 泛型支持 : 集合天生与泛型结合,保证了类型安全,避免了从集合中取出元素时的强制类型转换和潜在错误。
二、集合框架的核心:接口与实现 <🧱>
Java 集合框架的设计非常优雅,它主要由一系列接口(定义规范)和实现类(提供具体的数据结构和算法)组成。我们写代码时,应该尽量面向接口编程,这样更灵活。
核心接口主要有这几位大佬:
Collection<E>
<🧺>: 单列集合的根接口,定义了所有单列集合(一次存一个元素)的基本操作,如add()
,remove()
,contains()
,size()
,isEmpty()
,iterator()
。它派生出两个主要的子接口:List
和Set
。List<E>
<📝>: 有序的集合(元素按插入顺序排列),允许存储重复的元素。可以把它想象成一个带编号的清单,可以根据索引(编号)精确访问。Set<E>
<🚫>: 无序(通常不保证顺序)的集合,不允许存储重复的元素。就像一个独特的邮票收藏册 ,每张邮票都是独一无二的。Queue<E>
<🚶♀️➡️🚶♂️>: 队列接口,通常遵循先进先出(FIFO)的原则(也有例外,如优先队列)。就像排队买票 <🎫>,先来的先买。Map<K, V>
<🔑➡️<🎁>>: 映射接口,存储的是键值对 (Key-Value Pair)。注意⚠️:Map
不直接继承Collection
接口,它自成一派,但通常被认为是集合框架的一部分。Key 必须唯一,Value 可以重复。就像一本字典 📖,通过唯一的“单词”(Key)可以查到对应的“解释”(Value)。
接下来,我们详细看看最常用的 List
, Set
, Map
及其主要实现。
三、List
接口:有序,可重复 <📝>
特点:存入和取出的顺序一致,可以包含相同的元素。
常用实现类:
ArrayList<E>
<🧱>:- 底层:基于动态数组实现。
- 优点👍:查询(根据索引
get()
)速度快⚡️(随机访问快)。 - 缺点👎:增删(
add()
/remove()
,尤其是在中间位置)速度相对较慢🐢,因为可能需要移动大量元素。 - 适用场景🎯:查找多、增删少的情况。最常用的 List 实现。
LinkedList<E>
<🔗>:- 底层:基于双向链表实现。
- 优点👍:增删(尤其是在首尾)速度快🚀,不需要移动元素,只需修改指针。
- 缺点👎:查询(根据索引
get()
)速度相对较慢🐌,需要从头或尾开始遍历链表。 - 适用场景🎯:增删多、查找少的情况。它也实现了
Queue
接口,常被用作队列或栈。
代码示例 (ArrayList
):
import java.util.ArrayList;
import java.util.List;public class ListDemo {public static void main(String[] args) {// 创建一个存储 String 的 ArrayList (面向接口编程)List<String> names = new ArrayList<>(); // new// 添加元素names.add("Alice"); // 索引 0names.add("Bob"); // 索引 1names.add("Charlie"); // 索引 2names.add("Bob"); // 允许重复System.out.println("Names list: " + names); // 输出保持添加顺序// 获取元素 (按索引)String secondName = names.get(1); // 获取索引为 1 的元素System.out.println("Second name: " + secondName); // Bob// 修改元素names.set(3, "David"); // 修改索引为 3 的元素System.out.println("After modification: " + names);// 删除元素names.remove(0); // 删除索引为 0 的元素System.out.println("After removing first element: " + names);// 获取大小System.out.println("Current size: " + names.size());// 遍历 (增强 for)System.out.print("Iterating through names: ");for(String name : names) {System.out.print(name + " ");}System.out.println();}
}
四、Set
接口:无序 (通常),不重复 <🚫>
特点:不能包含重复的元素。大部分实现不保证元素的存取顺序。
常用实现类:
HashSet<E>
<🧺><#️⃣>:
- 底层:基于哈希表 (HashMap 实现)。
- 优点👍:添加、删除、查找(
contains()
)速度非常快⚡️(平均 O(1))。 - 缺点👎:不保证元素的顺序。
- 要求:存入
HashSet
的元素必须正确重写hashCode()
和equals()
方法。 - 适用场景🎯:需要快速去重或快速判断元素是否存在,且不关心顺序。最常用的 Set 实现。
代码示例 (HashSet
):
import java.util.HashSet;
import java.util.Set;public class HashSetDemo {public static void main(String[] args) {// 创建一个存储 Integer 的 HashSet (面向接口编程)Set<Integer> uniqueNumbers = new HashSet<>(); // new// 添加元素uniqueNumbers.add(5);uniqueNumbers.add(10);uniqueNumbers.add(5); // 尝试添加重复元素 5uniqueNumbers.add(15);uniqueNumbers.add(10); // 尝试添加重复元素 10// 输出 Set,重复元素自动被忽略,顺序不一定是添加顺序System.out.println("Unique numbers set: " + uniqueNumbers); // 可能输出 [5, 10, 15] 或其他顺序// 检查是否包含元素boolean hasTen = uniqueNumbers.contains(10);System.out.println("Set contains 10? " + hasTen); // true// 删除元素uniqueNumbers.remove(5);System.out.println("After removing 5: " + uniqueNumbers);System.out.println("Current size: " + uniqueNumbers.size()); // 2// 遍历 (顺序不保证)System.out.print("Iterating through the set: ");for(Integer num : uniqueNumbers) {System.out.print(num + " ");}System.out.println();}
}
LinkedHashSet<E>
<🔗><#️⃣>:
- 底层:基于哈希表和双向链表。
- 优点👍:既有
HashSet
的快速查找 (O(1)),又能保持元素的插入顺序 <➡️>!遍历时会按照添加的顺序输出。 - 缺点👎:性能略低于
HashSet
(因为维护链表的额外开销),内存占用也稍大。 - 要求:同样需要元素正确重写
hashCode()
和equals()
。 - 适用场景🎯:需要去重,同时希望保持元素添加时的顺序。
代码示例 (LinkedHashSet
):
import java.util.LinkedHashSet;
import java.util.Set;public class LinkedHashSetDemo {public static void main(String[] args) {// 创建 LinkedHashSet,保持插入顺序Set<String> orderedUniqueNames = new LinkedHashSet<>(); // neworderedUniqueNames.add("Charlie");orderedUniqueNames.add("Alice");orderedUniqueNames.add("Bob");orderedUniqueNames.add("Alice"); // 重复的 Alice 被忽略// 输出时会保持添加顺序!System.out.println("Ordered unique names: " + orderedUniqueNames);// Output: Ordered unique names: [Charlie, Alice, Bob]}
}
TreeSet<E>
<🌳>:
- 底层:基于红黑树 (TreeMap 实现)。
- 优点👍:元素会自动排序!可以按照元素的自然顺序(元素需实现
Comparable
接口)或者指定的比较器(创建TreeSet
时传入Comparator
)进行排序。 - 缺点👎:增删查的速度略慢于
HashSet
(O(log n))。 - 要求:存入
TreeSet
的元素必须是可比较的。 - 适用场景🎯:需要去重的同时保持元素有序。
代码示例 (TreeSet
):
import java.util.Set;
import java.util.TreeSet;public class TreeSetDemo {public static void main(String[] args) {// 创建 TreeSet,元素会自动排序Set<Integer> sortedNumbers = new TreeSet<>(); // newsortedNumbers.add(50);sortedNumbers.add(20);sortedNumbers.add(80);sortedNumbers.add(20); // 重复的 20 被忽略// 输出时元素是排序好的!System.out.println("Sorted unique numbers: " + sortedNumbers);// Output: Sorted unique numbers: [20, 50, 80]}
}
五、Map
接口:键值对存储 <🔑➡️<🎁>>
特点:存储的是 Key-Value 对,Key 必须唯一,Value 可以重复。通过 Key 可以快速查找到对应的 Value。
常用实现类:
HashMap<K, V>
<🧺><#️⃣>:
-
底层:基于哈希表。
-
优点👍:增删查(根据 Key)速度极快⚡️(平均 O(1))。
-
缺点👎:不保证键值对的存储顺序。
-
要求:Key 必须正确重写
hashCode()
和equals()
。 -
允许 Key 和 Value 为
null
(Key 只能有一个null
)。 -
适用场景🎯:需要快速 Key-Value 查找,不关心顺序。最常用。
-
LinkedHashMap<K, V>
<🔗><#️⃣>:- 底层:基于哈希表和双向链表。
- 优点👍:既有
HashMap
的快速查找,又能保持键值对的插入顺序 <➡️>(或访问顺序,构造时可指定)。 - 缺点👎:性能略低于
HashMap
,内存占用稍大。 - 要求:Key 同样需要
hashCode()
和equals()
。 - 适用场景🎯:需要保持插入顺序的映射关系,如实现LRU缓存(使用访问顺序)。
-
TreeMap<K, V>
<🌳>:- 底层:基于红黑树。
- 优点👍:键值对会根据 Key 自动排序!
- 缺点👎:增删查速度略慢(O(log n))。
- 要求:Key 必须是可比较的。
- 不允许 Key 为
null
。 - 适用场景🎯:需要按 Key 排序的映射关系。
-
Hashtable<K, V>
<🔒><#️⃣>:- 底层:也是基于哈希表。
- 特点:线程安全!所有方法都是
synchronized
的。 - 缺点👎:因为所有方法都加锁,并发性能很差 <🐢>。现在基本不推荐使用。
- 不允许 Key 或 Value 为
null
<🚫>。 - 替代方案💡:需要线程安全的 Map,优先考虑
<font color="purple">ConcurrentHashMap</font>
(来自java.util.concurrent
包),它提供了更好的并发性能。
(HashMap 和 TreeMap 示例省略,见上一版本)
六、 集合工具类:Collections
<🛠️>
别和 Collection
接口搞混了!Collections
(带 s) 是一个工具类,里面全是 static
方法,用来方便地操作集合。
一些常用方法:
Collections.sort(List<T> list)
: 对List
进行排序 <📊>。Collections.reverse(List<?> list)
: 反转List
中元素的顺序 <🔄>。Collections.shuffle(List<?> list)
: 随机打乱List
中元素的顺序 <🔀>。Collections.max(Collection<? extends T> coll)
/min(...)
: 找到集合中的最大/最小值 <🥇><🥉>。Collections.frequency(Collection<?> c, Object o)
: 计算指定元素在集合中出现的次数 <🔢>。Collections.synchronizedList/Set/Map(...)
: 返回指定集合的线程安全版本 <🔒> (性能不如java.util.concurrent
包下的类)。Collections.unmodifiableList/Set/Map(...)
: 返回指定集合的只读视图 <🚫>✍️。
代码示例:
import java.util.ArrayList;
import java.util.Collections; // 导入工具类
import java.util.List;public class CollectionsUtilDemo {public static void main(String[] args) {List<Integer> numbers = new ArrayList<>();numbers.add(5);numbers.add(1);numbers.add(8);numbers.add(3);System.out.println("Original list: " + numbers);// 排序Collections.sort(numbers);System.out.println("Sorted list: " + numbers);// 反转Collections.reverse(numbers);System.out.println("Reversed list: " + numbers);// 查找最大值Integer max = Collections.max(numbers);System.out.println("Maximum value: " + max);}
}
七、 别忘了迭代器 (Iterator
) <🔍>
迭代器是遍历所有 Collection
(List, Set, Queue) 的标准、通用方式。虽然增强型 for
循环更简洁,但迭代器允许你在遍历过程中安全地删除元素(使用 iterator.remove()
)。
(迭代器代码示例省略,见上一版本)
八、总结:选择合适的容器 🏁
Java 集合框架提供了一套丰富、灵活、高效的数据容器。选择时主要考虑:
- 是否有序? (
List
有序, 大部分Set
/Map
无序,LinkedHashSet
/LinkedHashMap
插入有序,TreeSet
/TreeMap
排序) - 是否允许重复? (
List
允许,Set
不允许) - 存储结构? (单个元素用
List
/Set
, 键值对用Map
) - 性能需求? (查找为主选
ArrayList
/HashSet
/HashMap
, 增删为主选LinkedList
, 需要排序选TreeSet
/TreeMap
) - 是否需要线程安全? (优先考虑
ConcurrentHashMap
等java.util.concurrent
包下的类)
核心原则:面向接口编程!始终使用泛型!根据需求选择最合适的实现类。
九、练练手,检验成果!✏️🧠
检验一下学习成果吧!
⭐ 选择与应用 ⭐
- 如果你需要存储用户访问网站的页面顺序(URL 字符串),并且可能会频繁在列表开头添加新的访问记录(最新的访问总在最前面),你会选择
ArrayList<String>
还是LinkedList<String>
?为什么? - 你需要存储一个班级所有学生的学号 (String),要求不能重复,并且希望能够快速检查某个学号是否已经存在,对顺序没有要求。选择哪个
Set
实现? - 你想记录每个单词 (String) 在一篇文章中出现的次数 (Integer)。选择哪个
Map
实现最合适? - 新增:如果第3题中,你希望输出单词及其次数时,单词能按照字母顺序排列,应该选择哪个
Map
实现? - 新增:你需要一个线程安全的
Map
来存储缓存数据,应该优先选择哪个类?
⭐ 工具类与遍历 ⭐
- 创建一个
ArrayList<Integer>
,添加一些数字,然后使用Collections.sort()
对其排序,并使用Collections.reverse()
将其反转,最后打印结果。 - 使用迭代器遍历一个
HashSet<String>
,并在遍历过程中安全地删除所有长度小于 5 的字符串。
⭐ 概念辨析 ⭐
Collection
和Collections
有什么区别?- 为什么
HashMap
的 Key 需要正确重写hashCode()
和equals()
方法?如果只重写equals()
而不重写hashCode()
会有什么问题? - 新增:
ArrayList
和Vector
有什么主要区别?为什么现在很少推荐使用Vector
?
十、参考答案 ✅💡
⭐ 选择与应用答案 ⭐
-
选择
LinkedList
。- 原因:需要在列表开头进行频繁的添加操作。
LinkedList
在首部添加元素的时间复杂度是 O(1),非常快。而ArrayList
在开头添加元素需要将所有现有元素向后移动,时间复杂度是 O(n),效率低。
- 原因:需要在列表开头进行频繁的添加操作。
-
选择
HashSet<String>
。- 原因:需求是去重 (
Set
特性),快速检查是否存在 (HashSet
提供 O(1) 的contains()
操作),且不关心顺序。HashSet
完美符合这些要求。
- 原因:需求是去重 (
-
选择
HashMap<String, Integer>
。- 原因:需要存储单词 (Key) 到出现次数 (Value) 的映射关系 (
Map
功能)。单词 (Key) 是唯一的。通常查找某个单词的次数(get(word)
)和更新次数(put(word, count + 1)
)需要快,HashMap
提供 O(1) 的平均性能。对单词的存储顺序通常不关心。
- 原因:需要存储单词 (Key) 到出现次数 (Value) 的映射关系 (
-
选择
TreeMap<String, Integer>
。- 原因:
TreeMap
会根据 Key (String
) 的自然顺序(字母顺序)自动对键值对进行排序。
- 原因:
-
优先选择
ConcurrentHashMap<K, V>
(来自java.util.concurrent
包)。- 原因:
ConcurrentHashMap
提供了高效的线程安全机制(如分段锁或 CAS),并发性能远好于使用全局锁的Hashtable
或Collections.synchronizedMap()
。
- 原因:
⭐ 工具类与遍历答案 ⭐
-
排序与反转 List:
import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Arrays; // 方便初始化public class SortReverseDemo {public static void main(String[] args) {List<Integer> numbers = new ArrayList<>(Arrays.asList(5, 1, 8, 3, 2));System.out.println("Original list: " + numbers);Collections.sort(numbers); // 排序System.out.println("Sorted list: " + numbers);Collections.reverse(numbers); // 反转System.out.println("Reversed list: " + numbers);} }
-
使用迭代器安全删除:
import java.util.HashSet; import java.util.Iterator; import java.util.Set;public class IteratorRemoveDemo {public static void main(String[] args) {Set<String> words = new HashSet<>();words.add("Java");words.add("is");words.add("fun");words.add("and");words.add("powerful");System.out.println("Original set: " + words);// 必须使用迭代器进行遍历中的删除操作Iterator<String> iterator = words.iterator();while (iterator.hasNext()) {String word = iterator.next();if (word.length() < 5) {// 安全删除当前元素iterator.remove(); // 使用迭代器的 remove() 方法System.out.println("Removed: " + word);}}// 不能在增强 for 循环中直接调用 words.remove(word),会抛 ConcurrentModificationExceptionSystem.out.println("Set after removal: " + words);} }
⭐ 概念辨析答案 ⭐
-
Collection
vsCollections
:Collection
: 是一个 接口 <📝>,单列集合的根。Collections
: 是一个 工具类 <🛠️>,提供操作集合的static
方法。
-
为什么
HashMap
Key 需要hashCode()
和equals()
: (答案同上一版本,此处省略) -
ArrayList
vsVector
:- 主要区别:
Vector
是线程安全的,它的所有方法都使用synchronized
修饰。ArrayList
是非线程安全的。此外,Vector
是 Java 早期 (JDK 1.0) 就存在的类,而ArrayList
是在集合框架 (JDK 1.2) 中引入的。Vector
的默认扩容大小通常是翻倍,而ArrayList
通常是增长 50%。 - 为什么少用
Vector
: 因为其全局同步导致性能低下 <🐢>,在不需要线程安全的场景下完全没必要用它(用ArrayList
更快)。而在需要线程安全的场景下,有性能更好的替代品,如使用Collections.synchronizedList()
包装ArrayList
(虽然锁粒度仍然较大),或者直接使用java.util.concurrent
包下的CopyOnWriteArrayList
(适用于读多写少的场景)。因此,Vector
现在基本被视为遗留类,在新代码中很少推荐使用。
- 主要区别:
集合框架是 Java 中使用极其频繁的部分,掌握好它对日常开发至关重要!希望这篇笔记能帮你理清思路。如果觉得有帮助,别忘了 点赞👍、收藏⭐、关注 哦! 😉