1. Java集合框架概述
在Java编程中,集合框架是我们日常开发中最常用的工具之一。Java 13中的集合框架延续了之前版本的优秀设计,提供了丰富的数据结构和算法实现。List和Set作为Collection接口的两个重要子接口,分别代表了有序可重复集合和无序不可重复集合这两种基础数据结构。
集合框架的核心价值在于:
- 提供高性能的数据结构实现
- 减少程序员需要编写的样板代码
- 提高代码的可读性和可维护性
- 通过统一的接口降低学习成本
我刚开始使用Java集合时,常常困惑于何时该用ArrayList,何时该用LinkedList。后来在实际项目中踩过几次坑后才明白,选择哪种集合实现,本质上取决于我们对数据操作的需求特点。
2. 数据结构基础
2.1 常见数据结构类型
Java集合框架底层实现了多种经典数据结构,理解这些基础数据结构的特点对于正确使用集合类至关重要:
数组(Array)
- 内存中连续存储的相同类型元素集合
- 优点:随机访问速度快(O(1))
- 缺点:大小固定,插入删除效率低(O(n))
- Java中的ArrayList就是基于数组实现的
// 数组基本用法示例 int[] arr = new int[10]; arr[0] = 1; // O(1)访问链表(Linked List)
- 通过节点指针连接的非连续存储结构
- 优点:插入删除效率高(O(1))
- 缺点:随机访问需要遍历(O(n))
- Java中的LinkedList就是双向链表的实现
// 链表节点典型结构 class Node { Object data; Node next; Node prev; }栈(Stack)
- 后进先出(LIFO)的数据结构
- 基本操作:push(压栈), pop(弹栈)
- Java中可用Deque接口实现栈功能
Deque<Integer> stack = new ArrayDeque<>(); stack.push(1); // 压栈 int top = stack.pop(); // 弹栈队列(Queue)
- 先进先出(FIFO)的数据结构
- 基本操作:enqueue(入队), dequeue(出队)
- Java中Queue接口定义了队列的基本行为
Queue<Integer> queue = new LinkedList<>(); queue.offer(1); // 入队 int head = queue.poll(); // 出队哈希表(Hash Table)
- 通过哈希函数计算存储位置的数据结构
- 理想情况下访问时间复杂度为O(1)
- Java中的HashMap就是哈希表的实现
红黑树(Red-Black Tree)
- 自平衡的二叉查找树
- 保证最坏情况下基本操作时间为O(log n)
- Java中的TreeMap就是基于红黑树实现的
2.2 数据结构选择策略
在实际开发中,选择数据结构需要考虑以下几个因素:
访问模式:
- 频繁随机访问 → 数组/ArrayList
- 频繁插入删除 → 链表/LinkedList
- 需要快速查找 → 哈希表/HashSet/HashMap
- 需要有序遍历 → 树结构/TreeSet/TreeMap
数据规模:
- 小规模数据 → 简单结构即可
- 大规模数据 → 需要考虑时间复杂度
线程安全:
- 单线程环境 → 普通集合
- 多线程环境 → Concurrent集合或同步包装
内存占用:
- 数组结构通常更节省内存
- 链表结构每个元素需要额外指针空间
提示:在不确定该用哪种集合时,可以从ArrayList开始,如果发现性能瓶颈再考虑其他实现。过早优化往往是浪费时间的根源。
3. List接口详解
3.1 List接口特点
List是Java集合框架中最常用的接口之一,它具有以下核心特性:
- 有序性:元素按照插入顺序保存,可以通过索引精确访问
- 可重复性:允许包含相同的元素
- 索引操作:提供基于位置(index)的访问和操作方法
List<String> list = new ArrayList<>(); list.add("A"); // 索引0 list.add("B"); // 索引1 list.add("A"); // 允许重复元素 String element = list.get(1); // 获取索引1的元素"B"List接口的常用实现类:
| 实现类 | 底层结构 | 特点 | 适用场景 |
|---|---|---|---|
| ArrayList | 动态数组 | 随机访问快,插入删除慢 | 查询多,增删少 |
| LinkedList | 双向链表 | 插入删除快,随机访问慢 | 频繁增删 |
| Vector | 动态数组 | 线程安全,性能较差 | 需要线程安全 |
| CopyOnWriteArrayList | 动态数组 | 写时复制,线程安全 | 读多写少并发场景 |
3.2 ArrayList深度解析
ArrayList是最常用的List实现,其内部通过动态数组实现:
// ArrayList核心字段 transient Object[] elementData; // 存储元素的数组 private int size; // 当前元素数量扩容机制:
- 默认初始容量为10
- 添加元素时检查容量,不足则扩容
- 新容量 = 旧容量 * 1.5
- 将旧数组元素拷贝到新数组
// ArrayList扩容关键代码 private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; elementData = Arrays.copyOf(elementData, newCapacity); }性能特点:
- 随机访问:O(1)
- 末尾添加:平均O(1),最坏O(n)(需要扩容)
- 中间插入/删除:O(n)(需要移动元素)
使用建议:
- 预估数据量大小,构造时指定初始容量避免频繁扩容
List<Integer> list = new ArrayList<>(1000); // 指定初始容量 - 批量添加数据时使用addAll而不是循环add
- 遍历时使用迭代器或增强for循环而非索引访问(LinkedList情况下)
3.3 LinkedList特性分析
LinkedList基于双向链表实现,每个节点包含前驱和后继指针:
// LinkedList节点结构 private static class Node<E> { E item; Node<E> next; Node<E> prev; // 构造方法... }特有方法:
- addFirst/addLast:在首尾添加元素
- getFirst/getLast:获取首尾元素
- removeFirst/removeLast:移除首尾元素
- push/pop:实现栈操作
LinkedList<String> list = new LinkedList<>(); list.addFirst("A"); // 添加到头部 list.addLast("B"); // 添加到尾部 String first = list.getFirst(); // 获取头部元素性能特点:
- 头部/尾部插入删除:O(1)
- 随机访问:O(n)
- 中间插入:O(n)(需要先找到位置)
使用场景:
- 需要实现队列或栈功能
- 频繁在集合中间插入删除元素
- 不需要随机访问或随机访问较少
3.4 List常见问题与解决方案
问题1:遍历时修改集合
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); for (String s : list) { if (s.equals("B")) { list.remove(s); // 抛出ConcurrentModificationException } }解决方案:
- 使用迭代器的remove方法
Iterator<String> it = list.iterator(); while (it.hasNext()) { if (it.next().equals("B")) { it.remove(); // 安全删除 } } - 使用CopyOnWriteArrayList(线程安全)
- 使用Java 8+的removeIf方法
list.removeIf(s -> s.equals("B"));
问题2:子列表的陷阱
subList返回的是原列表的视图,对子列表的修改会影响原列表:
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4)); List<Integer> subList = list.subList(1, 3); subList.set(0, 9); // 原列表变为[1, 9, 3, 4]解决方案:
- 需要独立子列表时创建新集合:
List<Integer> independentSubList = new ArrayList<>(list.subList(1, 3));
4. Set接口解析
4.1 Set接口特性
Set接口与List的主要区别:
| 特性 | List | Set |
|---|---|---|
| 顺序 | 有序 | 一般无序(TreeSet除外) |
| 重复 | 允许 | 不允许 |
| 空元素 | 允许多个null | 最多一个null |
| 实现类 | ArrayList, LinkedList等 | HashSet, TreeSet等 |
Set的常用实现类:
| 实现类 | 底层实现 | 特点 | 排序 |
|---|---|---|---|
| HashSet | 哈希表 | 最快查找 | 无序 |
| LinkedHashSet | 哈希表+链表 | 保持插入顺序 | 插入顺序 |
| TreeSet | 红黑树 | 自动排序 | 自然顺序或Comparator |
4.2 HashSet实现原理
HashSet是基于HashMap实现的,其核心在于哈希算法和equals/hashCode方法:
// HashSet的简化实现 public class HashSet<E> { private transient HashMap<E, Object> map; private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT) == null; } // 其他方法... }元素唯一性保证:
- 添加元素时先计算hashCode
- 根据hashCode定位到哈希桶
- 如果桶为空,直接存入
- 如果桶不为空,调用equals方法比较
- equals返回true则认为元素已存在,不添加
hashCode与equals契约:
- 两个对象equals为true,则hashCode必须相同
- hashCode相同的对象,equals不一定为true
- 重写equals必须重写hashCode
// 正确实现hashCode和equals的例子 class Student { String name; int age; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Student)) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(name, age); } }4.3 TreeSet排序机制
TreeSet基于TreeMap实现,元素要么实现Comparable接口,要么提供Comparator:
// 自然排序示例 Set<String> set = new TreeSet<>(); set.add("Banana"); set.add("Apple"); set.add("Orange"); // 输出顺序为Apple, Banana, Orange // 自定义排序示例 Set<Integer> customSet = new TreeSet<>((a, b) -> b - a); // 降序 customSet.add(3); customSet.add(1); customSet.add(2); // 输出顺序为3, 2, 1性能考虑:
- HashSet操作通常为O(1)
- TreeSet操作通常为O(log n)
- LinkedHashSet比HashSet稍慢,但保持插入顺序
4.4 Set使用最佳实践
去重应用:
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5); Set<Integer> uniqueNumbers = new HashSet<>(numbers); // 自动去重集合运算:
Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3)); Set<Integer> set2 = new HashSet<>(Arrays.asList(2, 3, 4)); set1.retainAll(set2); // 交集,set1变为[2, 3] set1.addAll(set2); // 并集 set1.removeAll(set2); // 差集性能优化:
- 对于自定义对象,确保hashCode方法分布均匀
- 预估元素数量,设置合适的初始容量和负载因子
// 预估1000个元素,负载因子0.75 Set<String> optimizedSet = new HashSet<>(1333, 0.75f);
5. Collections工具类
5.1 常用工具方法
Collections类提供了丰富的静态方法来操作集合:
排序操作:
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9); Collections.sort(numbers); // 自然排序[1, 1, 3, 4, 5, 9] Collections.sort(numbers, Collections.reverseOrder()); // 逆序[9, 5, 4, 3, 1, 1] Collections.shuffle(numbers); // 随机打乱不可变集合:
List<String> immutableList = Collections.unmodifiableList(Arrays.asList("A", "B", "C")); // immutableList.add("D"); // 抛出UnsupportedOperationException同步包装:
List<String> syncList = Collections.synchronizedList(new ArrayList<>()); // 线程安全的列表其他实用方法:
Collections.reverse(list); // 反转列表 Collections.rotate(list, 2); // 旋转元素 Collections.swap(list, 0, 1); // 交换元素 Collections.fill(list, "default"); // 填充默认值 int frequency = Collections.frequency(list, "A"); // 出现次数5.2 Comparator高级用法
Java 8引入了更简洁的Comparator写法:
List<Person> people = Arrays.asList( new Person("Alice", 30), new Person("Bob", 25), new Person("Charlie", 35) ); // 多字段排序 people.sort(Comparator .comparing(Person::getAge) // 先按年龄 .thenComparing(Person::getName)); // 再按姓名 // 复杂排序 people.sort(Comparator .comparing(Person::getName, String.CASE_INSENSITIVE_ORDER.reversed()) .thenComparingInt(Person::getAge));自定义Comparator:
Comparator<String> lengthComparator = (s1, s2) -> { int lenDiff = s1.length() - s2.length(); return lenDiff != 0 ? lenDiff : s1.compareTo(s2); }; List<String> strings = Arrays.asList("apple", "banana", "pear", "kiwi"); strings.sort(lengthComparator); // 排序结果:kiwi, pear, apple, banana5.3 集合性能优化技巧
批量操作:
// 差的做法:多次扩容 List<Integer> list = new ArrayList<>(); for (int i = 0; i < 10000; i++) { list.add(i); } // 好的做法:一次性添加 List<Integer> betterList = new ArrayList<>(10000); betterList.addAll(IntStream.range(0, 10000).boxed().collect(Collectors.toList()));选择合适集合:
- 随机访问多 → ArrayList
- 频繁插入删除 → LinkedList
- 需要去重 → HashSet
- 需要排序 → TreeSet
- 需要保持插入顺序 → LinkedHashSet
避免装箱拆箱:
// 使用原始类型专用集合 IntArrayList fastList = new IntArrayList(); fastList.add(1); // 无装箱开销并行处理:
List<String> bigList = ...; // 并行流处理 bigList.parallelStream() .filter(s -> s.length() > 5) .map(String::toUpperCase) .collect(Collectors.toList());
6. Java 13中的集合新特性
虽然Java 13在集合框架方面没有引入重大改变,但有一些值得注意的改进:
动态CDS归档:
- 提升集合类加载性能
- 减少JVM启动时间
微优化:
- ArrayList和HashMap等常用类的小幅性能优化
- 减少内存占用
预览特性影响:
- 文本块(Text Blocks)特性影响字符串集合的处理
List<String> sqlQueries = List.of( """ SELECT * FROM users WHERE age > 18 ORDER BY name """, """ UPDATE products SET price = price * 1.1 WHERE category = 'electronics' """ );Unicode 12.1支持:
- 影响字符串比较和排序
- TreeSet等依赖比较的集合可能表现不同
在实际项目中,我发现Java 13的ZGC垃圾收集器对内存密集型集合应用特别有帮助,可以显著减少大型集合操作时的停顿时间。