Java 集合详解:知识点与注意事项
一、Java 集合框架概述
Java 集合框架(Java Collections Framework)是Java语言中一个非常重要且强大的工具库,它位于java.util包下,包含了一系列用于存储和操作对象的接口和类。这个框架为开发者提供了高效、灵活的数据结构实现,大大简化了集合操作。
该框架主要分为两大体系:
1.Collection 接口体系 Collection 接口是所有集合类的根接口之一,它用于存储和处理单个元素对象。在实际开发中,我们通常使用它的三个主要子接口:
-
List(列表):
- 特点:元素有序(存入顺序和取出顺序一致),允许重复元素
- 典型实现:
- ArrayList:基于动态数组实现,随机访问效率高
- LinkedList:基于双向链表实现,插入删除效率高
- Vector:线程安全的动态数组实现
-
Set(集合):
- 特点:元素无序(具体实现类可能有特定排序规则),不允许重复元素
- 典型实现:
- HashSet:基于哈希表实现,查询效率高
- TreeSet:基于红黑树实现,元素自动排序
- LinkedHashSet:维护插入顺序的HashSet
-
Queue(队列):
- 特点:通常遵循先进先出(FIFO)原则
- 典型实现:
- LinkedList:可用作队列实现
- PriorityQueue:优先级队列,元素按优先级排序
- ArrayDeque:双端队列实现
2.Map 接口体系 Map接口存储的是键值对(key-value)映射,其中:
- 键(key)不允许重复,每个键最多映射到一个值
- 值(value)可以重复
常用实现类包括:
- HashMap:基于哈希表实现,查询效率高
- TreeMap:基于红黑树实现,按键的自然顺序或Comparator排序
- LinkedHashMap:维护插入顺序的HashMap
- Hashtable:线程安全的Map实现(较老的实现)
这些集合类在Java开发中的应用场景非常广泛:
- ArrayList常用于需要频繁随机访问元素的场景
- LinkedList适合频繁插入删除操作的场景
- HashSet用于需要快速判断元素是否存在的场景
- HashMap是缓存实现、对象映射等的首选数据结构
二、常用集合类详解
一、List接口实现类
1. ArrayList
特点与实现细节
ArrayList是基于动态数组实现的List接口,其内部使用Object[] elementData数组存储元素。主要特点包括:
-
底层实现:
- 使用可动态扩容的Object数组
- 数组类型为
transient Object[] elementData,支持序列化优化
-
查询效率:
- 随机访问时间复杂度O(1),直接通过索引计算内存地址
- 示例:
list.get(100)可以直接定位到第101个元素
-
增删效率:
- 末尾添加元素平均时间复杂度O(1)
- 中间插入/删除需要移动后续元素,最坏情况O(n)
- 示例:在包含10000个元素的列表首位插入元素,需要移动9999个元素
-
线程安全:
- 非线程安全,多线程并发修改会抛出ConcurrentModificationException
- 替代方案:
或者使用List<String> syncList = Collections.synchronizedList(new ArrayList<>());CopyOnWriteArrayList
-
扩容机制:
- 默认初始容量10
- 扩容公式:
newCapacity = oldCapacity + (oldCapacity >> 1)(即1.5倍) - 扩容时调用
Arrays.copyOf()创建新数组并复制元素 - 最大容量
Integer.MAX_VALUE - 8(部分JVM实现保留头信息)
使用场景
适合以下场景:
- 需要频繁随机访问元素
- 主要在列表末尾进行添加操作
- 元素数量相对稳定或可预估
- 内存空间较为充足
基础操作示例
// 创建ArrayList的四种方式
List<String> list1 = new ArrayList<>(); // 默认构造:初始容量10
List<String> list2 = new ArrayList<>(20); // 指定初始容量
List<String> list3 = new ArrayList<>(existingCollection); // 从集合创建
List<String> list4 = List.of("A", "B", "C"); // Java9+不可变列表
// 容量优化实践
list1.ensureCapacity(100); // 提前分配足够空间
// 添加大量元素...
list1.trimToSize(); // 调整为实际大小
// 批量操作示例
List<String> letters = Arrays.asList("A", "B", "C", "D", "E");
list1.addAll(letters); // 添加集合
list1.removeAll(Arrays.asList("A", "E")); // 删除指定元素
list1.retainAll(Arrays.asList("B", "C")); // 保留交集
// Java8+新特性
list1.removeIf(e -> e.length() > 5); // 条件删除
list1.replaceAll(String::toUpperCase); // 元素转换
list1.sort(Comparator.naturalOrder()); // 排序
// 并行流处理
list1.parallelStream()
.filter(s -> s.startsWith("A"))
.forEach(System.out::println);
性能优化建议
- 预估大小并设置初始容量,避免多次扩容
- 批量操作优先使用
addAll()而不是循环添加 - 频繁删除时考虑使用迭代器
remove()方法 - 只读场景可使用
Collections.unmodifiableList()包装
2. LinkedList
特点与实现细节
LinkedList是基于双向链表实现的List和Deque接口,其内部使用Node<E>节点存储元素。主要特点包括:
-
底层实现:
- 每个节点包含:
E item、Node<E> next、Node<E> prev - 维护
Node<E> first和Node<E> last指针 - 内存占用比ArrayList多50%以上(每个元素需要前后节点引用)
- 每个节点包含:
-
查询效率:
- 按索引访问需要遍历,时间复杂度O(n)
- 优化:索引在前半部分从前往后,后半部分从后往前
- 示例:10000个元素的列表,访问第5000个元素需要遍历5000次
-
增删效率:
- 已知位置插入/删除只需修改相邻节点引用,O(1)时间
- 实际应用中仍可能因查找位置产生O(n)开销
- 首尾操作效率最高,特别适合实现队列和栈
-
线程安全:
- 非线程安全,多线程环境需要外部同步
- 替代方案:使用
ConcurrentLinkedDeque
使用场景
适合以下场景:
- 频繁在列表两端进行增删操作
- 需要实现栈、队列或双端队列功能
- 元素数量变化较大且无法预估
- 内存充足但访问模式主要是顺序访问
特有方法示例
LinkedList<String> linkedList = new LinkedList<>();
// 队列操作(FIFO)
linkedList.offer("A"); // 添加元素到队尾
linkedList.offerLast("B"); // 同offer()
String head = linkedList.poll(); // 移除并返回队首
String peek = linkedList.peek(); // 查看队首不移除
// 双端队列操作
linkedList.offerFirst("C"); // 添加到队首
linkedList.pollLast(); // 移除并返回队尾
// 栈操作(LIFO)
linkedList.push("D"); // 压栈(添加到队首)
String top = linkedList.pop(); // 弹栈(移除队首)
// 其他特有方法
Iterator<String> descIt = linkedList.descendingIterator(); // 反向迭代
String first = linkedList.peekFirst(); // 查看队首
String last = linkedList.peekLast(); // 查看队尾
linkedList.removeFirstOccurrence("A"); // 删除第一次出现的元素
linkedList.removeLastOccurrence("B"); // 删除最后一次出现的元素
性能比较
| 操作 | ArrayList | LinkedList |
|---|---|---|
| get(index) | O(1) | O(n) |
| add(element) | O(1)均摊 | O(1) |
| add(index,element) | O(n) | O(n) |
| remove(index) | O(n) | O(n) |
| remove(element) | O(n) | O(n) |
| iterator.remove() | O(n) | O(1) |
3. Vector
特点与实现细节
Vector是Java早期线程安全的动态数组实现,现已不推荐使用。主要特点包括:
-
线程安全:
- 所有public方法都使用
synchronized修饰 - 同步粒度较粗,性能较差
- 现代替代方案:
或者使用List<String> list = Collections.synchronizedList(new ArrayList<>());CopyOnWriteArrayList
- 所有public方法都使用
-
扩容机制:
- 默认初始容量10
- 默认扩容为原容量的2倍
- 可通过
capacityIncrement构造参数调整扩容步长 - 设置为0时则每次翻倍扩容
-
遗留方法:
elements():返回Enumeration接口实现Enumeration<String> en = vector.elements(); while(en.hasMoreElements()) { String e = en.nextElement(); }addElement(E)/removeElement():与add/remove功能相同
-
子类Stack:
- 继承Vector实现的栈结构
- 提供push/pop/peek等栈操作
- 由于继承Vector导致性能问题,推荐使用
Deque接口实现:Deque<String> stack = new ArrayDeque<>();
与现代集合对比
| 特性 | Vector | ArrayList + Collections.synchronizedList |
|---|---|---|
| 线程安全 | 是(方法级同步) | 是(对象级同步) |
| 迭代器安全 | 否 | 需要手动同步 |
| 性能 | 较差 | 较好 |
| 扩容策略 | 2倍或自定义 | 1.5倍 |
| 遗留接口 | 支持Enumeration | 不支持 |
使用建议
- 新代码应避免直接使用Vector
- 需要线程安全列表时优先考虑:
// 读多写少场景 List<String> list1 = new CopyOnWriteArrayList<>(); // 通用场景 List<String> list2 = Collections.synchronizedList(new ArrayList<>()); - 需要栈功能时使用
ArrayDeque:Deque<String> stack = new ArrayDeque<>(); stack.push("A"); String top = stack.pop();
二、Set接口实现类
HashSet
实现原理
HashSet是基于HashMap实现的集合类,其核心存储机制实际上是使用HashMap的key来存储元素(value统一使用一个静态常量对象PRESENT占位)。
详细存储结构
// HashSet内部实际上使用HashMap存储
private transient HashMap<E,Object> map;
// 所有元素对应的value都是这个固定对象
private static final Object PRESENT = new Object();
哈希冲突处理机制
在哈希冲突处理方面:
-
JDK7及之前版本完全采用链表法解决冲突
- 当多个元素哈希到同一个数组索引时,形成单向链表
- 新元素插入链表头部(头插法)
-
JDK8进行了优化:
- 当链表长度超过8且数组长度达到64时,会将链表转为红黑树结构
- 转换阈值:TREEIFY_THRESHOLD = 8
- 最小数组长度要求:MIN_TREEIFY_CAPACITY = 64
- 当红黑树节点数小于6时,会退化为链表
- 退化阈值:UNTREEIFY_THRESHOLD = 6
- 当链表长度超过8且数组长度达到64时,会将链表转为红黑树结构
扩容机制
初始容量默认为16(与HashMap一致),负载因子为0.75。当元素数量超过(容量×负载因子)时,会自动扩容为原来的2倍。
扩容过程示例:
- 初始容量16,当元素数量达到12(16×0.75)时触发扩容
- 新容量变为32,重新hash所有元素
- 扩容后元素重新分布到更大的数组中
元素要求
为了正确使用HashSet,自定义类必须正确重写hashCode()和equals()方法:
class Person {
String name;
int age;
// 使用Objects工具类生成hashCode
@Override
public int hashCode() {
return Objects.hash(name, age); // 确保相同对象的hashCode相同
}
// 实现equals方法
@Override
public boolean equals(Object o) {
// 1. 检查是否是同一对象
if (this == o) return true;
// 2. 检查是否为null或类型不匹配
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换后比较关键字段
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
}
hashCode()方法最佳实践
- 使用相同的字段计算hashCode和equals
- 使用Objects.hash()简化实现
- 避免hashCode()方法过于简单(如只用一个字段)
- 避免hashCode()方法过于复杂(影响性能)
equals()方法实现要点
- 自反性:x.equals(x)必须返回true
- 对称性:x.equals(y)与y.equals(x)结果相同
- 传递性:如果x.equals(y)且y.equals(z),则x.equals(z)
- 一致性:多次调用结果相同
- 非空性:x.equals(null)返回false
TreeSet
排序方式
TreeSet支持两种排序方式:
1. 自然排序:元素实现Comparable接口
class Student implements Comparable<Student> {
private String name;
private int score;
// 按姓名升序排列
@Override
public int compareTo(Student s) {
return this.name.compareTo(s.name);
}
}
自然排序实现要点:
- 必须实现Comparable接口
- compareTo方法必须与equals方法保持一致
- 推荐实现Comparable的类同时重写equals和hashCode方法
2. 定制排序:创建TreeSet时传入Comparator
// 先按字符串长度排序,长度相同则按字典序
TreeSet<String> ts = new TreeSet<>(
(s1, s2) -> s1.length() != s2.length() ?
s1.length() - s2.length() : s1.compareTo(s2)
);
// 更复杂的比较器示例
Comparator<Employee> comp = Comparator
.comparing(Employee::getDepartment)
.thenComparingInt(Employee::getSalary)
.reversed();
TreeSet<Employee> empSet = new TreeSet<>(comp);
Comparator常用方法:
- comparing():指定比较属性
- thenComparing():添加次要比较条件
- reversed():反转排序顺序
- nullsFirst()/nullsLast():处理null值
时间复杂度分析
基于红黑树实现的时间复杂度:
- 插入(add):O(log n)
- 删除(remove):O(log n)
- 查找(contains):O(log n)
有序操作:
- first()/last():O(1) - 直接获取最小/最大节点
- lower()/higher():O(log n) - 查找前驱/后继节点
- headSet()/tailSet():O(1) - 创建子集视图
LinkedHashSet
继承关系
- 继承自HashSet
- 底层使用LinkedHashMap存储元素
顺序维护机制
- 通过双向链表维护元素的插入顺序
- 每个Entry节点包含before和after指针
// LinkedHashSet内部维护的Entry结构
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);
}
}
性能特点
-
插入和查询性能略低于HashSet
- 插入:需要额外维护链表指针
- 查询:与HashSet相同,但缓存局部性稍差
-
迭代效率高于HashSet
- 直接按链表顺序遍历,时间复杂度O(n)
- HashSet迭代需要遍历整个哈希表,性能较差
-
内存占用高于HashSet
- 每个元素需要额外存储before和after指针
- 内存开销增加约20-30%
使用场景
当需要Set的特性又需要维护插入顺序时使用,例如:
// 保持访问顺序的URL记录
LinkedHashSet<String> visitedUrls = new LinkedHashSet<>();
// 需要按添加顺序处理的唯一元素集合
LinkedHashSet<Task> taskQueue = new LinkedHashSet<>();
// 实现LRU缓存
class LRUCache<K,V> extends LinkedHashMap<K,V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > capacity;
}
}
典型应用场景
- 需要维护元素插入顺序的集合
- 实现简单的LRU缓存机制
- 需要快速判断元素存在性又要保持顺序的场景
- 需要去重且保留原始顺序的数据处理
三、Map接口实现类
HashMap
核心参数详解:
-
DEFAULT_INITIAL_CAPACITY = 16
- 默认初始容量,必须是2的幂次方(1<<4),这样设计是为了优化哈希计算
- 实际应用中可以指定初始容量(如
new HashMap(32)),但构造器会将其调整为最接近的2的幂
-
DEFAULT_LOAD_FACTOR = 0.75f
- 空间和时间成本的折衷值,表示哈希表填充程度达到75%时触发扩容
- 示例:容量16的HashMap在存储第12个元素(16*0.75)时会扩容
-
树化相关参数
TREEIFY_THRESHOLD=8:链表长度超过8时可能转为红黑树UNTREEIFY_THRESHOLD=6:树节点数小于等于6时转回链表MIN_TREEIFY_CAPACITY=64:哈希表总容量需达到64才会树化- 设计考虑:避免在哈希表较小时过早树化,节省内存
put方法完整流程:
-
哈希计算
// 扰动函数:高16位异或低16位,增强散列性 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } -
初始化检查
- 检测
Node<K,V>[] table是否为空 - 首次put时调用
resize()初始化数组(默认16容量)
- 检测
-
定位桶位置
- 通过
(table.length - 1) & hash计算索引(等价于取模但效率更高)
- 通过
-
冲突处理场景:
- 场景1:桶为空 → 直接新建节点插入
- 场景2:键已存在 → 覆盖原有value
- 场景3:树节点 → 调用
putTreeVal插入红黑树 - 场景4:链表 → 尾插法插入,同时检查:
- 如果链表长度≥8且表容量≥64 → 调用
treeifyBin树化 - 否则仅扩容
- 如果链表长度≥8且表容量≥64 → 调用
-
扩容机制
- 触发条件:
size > threshold(capacity * loadFactor) - 扩容操作:新建2倍大小的数组,重新分配节点(可能拆分树)
- 触发条件:
并发问题深度分析:
-
JDK7死循环场景:
// 多线程同时扩容时,头插法可能导致环形链表 void transfer(Entry[] newTable) { Entry<K,V> e = table[bucketIndex]; while(null != e) { Entry<K,V> next = e.next; // 线程A执行到此挂起 e.next = newTable[bucketIndex]; // 线程B完成转移后形成环 newTable[bucketIndex] = e; e = next; } } -
JDK8改进与局限:
- 改用尾插法避免环形链表
- 但仍存在数据覆盖问题(如两个线程同时put时可能丢失修改)
-
线程安全方案:
ConcurrentHashMap:分段锁+CAS(JDK7)或 synchronized+CAS(JDK8)Collections.synchronizedMap:使用互斥锁包装整个Map
TreeMap
红黑树规则验证:
-
染色规则示例:
- 插入新节点默认为红色(减少黑高调整)
- 若父节点为红,则通过旋转(左旋/右旋)和变色维持平衡
-
平衡性维护:
// 插入后的修复操作(包含三种case处理) private void fixAfterInsertion(Entry<K,V> x) { while (x != null && x != root && x.parent.color == RED) { if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { Entry<K,V> y = rightOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { // Case1: 叔叔节点为红 setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == rightOf(parentOf(x))) { // Case2: 形成三角关系 x = parentOf(x); rotateLeft(x); } // Case3: 直线关系 setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } // 对称处理右子树情况... } root.color = BLACK; }
导航方法应用场景:
-
区间查询:
// 查找最接近100的键 Map.Entry<Integer, String> floor = map.floorEntry(100); // ≤100的最大键 Map.Entry<Integer, String> ceiling = map.ceilingEntry(100); // ≥100的最小键 -
排行榜实现:
// 获取大于90分的最低记录 Map.Entry<Student, Integer> higher = gradeMap.higherEntry( new Student("", 90)); // 需实现Comparable
LinkedHashMap
访问顺序模式实现:
-
LRU缓存示例:
// 固定大小的LRU缓存 new LinkedHashMap<String, Object>(100, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > MAX_ENTRIES; } }; -
链表维护机制:
- Entry扩展:增加
before/after指针形成双向链表 - 访问回调:
afterNodeAccess方法将访问的节点移到链表尾部 - 插入回调:
afterNodeInsertion可触发 eldest 条目移除(需重写removeEldestEntry)
- Entry扩展:增加
Hashtable 对比分析
详细差异对比表:
| 特性 | Hashtable | HashMap |
|---|---|---|
| 线程安全 | 方法级synchronized锁 | 非线程安全 |
| Null处理 | 不允许null键值 | 允许null键值 |
| 初始容量 | 11(质数) | 16(2的幂) |
| 扩容方式 | 2n+1(保持质数) | 2n(保持2的幂) |
| 迭代器 | 枚举器+Iterator(fail-fast) | Iterator(fail-fast) |
| 哈希计算 | 直接使用hashCode() | 扰动函数优化哈希 |
| 继承体系 | Dictionary抽象类 | AbstractMap |
使用建议:
- 遗留系统:Hashtable在Java 1.0引入,新代码建议使用
ConcurrentHashMap - 特殊需求:当需要保证强一致性的旧代码维护时使用Hashtable
- 性能考量:Hashtable的全表锁在高并发时性能差,平均吞吐量比
ConcurrentHashMap低5~10倍
三、集合使用注意事项
线程安全问题
Java 集合框架中的大多数实现类(如 ArrayList、LinkedList、HashSet、HashMap 等)都是线程不安全的,这意味着在多线程并发环境下,如果多个线程同时访问和修改同一个集合实例,可能会导致数据不一致或其他不可预期的行为。
解决方案:
-
使用
Collections.synchronizedXXX()方法包装非线程安全集合:List<String> syncList = Collections.synchronizedList(new ArrayList<>()); Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>()); -
使用并发包(java.util.concurrent)中的线程安全集合:
ConcurrentHashMap:线程安全的哈希表实现CopyOnWriteArrayList:写时复制的线程安全列表CopyOnWriteArraySet:写时复制的线程安全集合
应用场景:
- 在 Web 应用中处理并发请求时
- 多线程数据处理程序中
- 缓存实现中需要共享数据访问时
集合元素的类型安全
泛型集合可以避免类型转换错误,但需要注意:
// 正确用法
List<String> stringList = new ArrayList<>();
stringList.add("Java");
String s = stringList.get(0); // 无需强制类型转换
// 错误用法可能引发 ClassCastException
List rawList = new ArrayList();
rawList.add("Java");
Integer i = (Integer)rawList.get(0); // 运行时错误
hashCode() 和 equals() 方法规范
对于要存储在 HashSet 或作为 HashMap 键的对象,必须正确实现这两个方法:
public class Student {
private String id;
private String name;
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Student other = (Student)obj;
return Objects.equals(id, other.id) &&
Objects.equals(name, other.name);
}
}
重要规则:
- 如果
a.equals(b)返回 true,那么a.hashCode()必须等于b.hashCode() - 相等的对象必须具有相同的哈希码
- 不相等的对象可以有相同的哈希码(哈希冲突)
集合遍历的最佳实践
不同的集合类型适合不同的遍历方式:
-
ArrayList:
// 索引遍历 - 效率最高 for (int i = 0; i < list.size(); i++) { String item = list.get(i); } -
LinkedList:
// 迭代器遍历 - 效率最高 Iterator<String> it = linkedList.iterator(); while (it.hasNext()) { String item = it.next(); } -
所有集合的通用遍历方式:
// 增强for循环(内部使用迭代器) for (String item : collection) { // 处理元素 }
遍历时修改集合的注意事项:
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if ("B".equals(item)) {
it.remove(); // 正确方式 - 使用迭代器的remove方法
// list.remove(item); // 错误 - 会抛出ConcurrentModificationException
}
}
集合初始化容量优化
对于基于数组实现的集合,合理设置初始容量可以避免频繁扩容:
// 已知最终大小约为1000时
List<String> list = new ArrayList<>(1000);
Map<String, Integer> map = new HashMap<>(1000);
// HashMap容量计算(考虑负载因子0.75)
int expectedSize = 1000;
int initialCapacity = (int) (expectedSize / 0.75 + 1);
Map<String, Integer> optimizedMap = new HashMap<>(initialCapacity);
扩容代价:
- ArrayList 每次扩容需要复制整个数组
- HashMap 扩容需要重新计算所有元素的哈希位置
TreeSet/TreeMap 的排序要求
元素必须满足以下条件之一:
- 实现
Comparable接口 - 构造集合时提供
Comparator
// 方式1:元素实现Comparable
class Person implements Comparable<Person> {
private String name;
@Override
public int compareTo(Person o) {
return this.name.compareTo(o.name);
}
}
TreeSet<Person> set1 = new TreeSet<>();
// 方式2:提供Comparator
TreeSet<String> set2 = new TreeSet<>(
(s1, s2) -> s1.length() - s2.length()
);
集合判空的最佳实践
// 不推荐 - 可能NPE
if (collection.size() == 0) { ... }
// 推荐方式
if (collection == null || collection.isEmpty()) { ... }
// Java8+ 更安全的做法
Optional.ofNullable(collection)
.filter(c -> !c.isEmpty())
.ifPresent(c -> {
// 处理非空集合
});
注意:isEmpty() 方法通常比 size() == 0 更高效,因为某些集合实现可能有更优化的判空方式。
更多推荐
所有评论(0)