集合框架全景图
Java集合就像是各种容器,数组是固定大小的箱子,集合是能自动伸缩的魔法口袋
顶层接口是 Collection,下面分三大家族:
List:有序、可重复(像排队,每个人有编号,允许同名)
Set:无序、不重复(像一堆弹珠,不关心顺序,不允许重复)
Queue:先进先出(像排队买奶茶,先来先服务)
文字版层次结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Iterable └── Collection ├── List(有序可重复) │ ├── ArrayList ← 最常用,底层数组 │ ├── LinkedList ← 底层链表,也实现了Queue │ └── Vector ← 线程安全但过时,别用 ├── Set(无序不重复) │ ├── HashSet ← 最常用,底层 │ ├── TreeSet ← 有序,底层红黑树 │ └── LinkedHashSet ← 保留插入顺序 └── Queue(先进先出) ├── LinkedList ← 同时实现和 ├── PriorityQueue ← 优先级队列 └── ArrayDeque ← 双端队列,比快
|
另外还有个独立门派 Map体系,不属于Collection,但也是集合框架的一部分
List:有序可重复
像一个有编号的清单,每个元素都有下标(从0开始),可以有重复元素
ArrayList —— 最常用的集合,没有之一
底层是数组,查询快(通过下标直接定位),增删慢(要移动后面的元素)
默认初始容量10,扩容时变成原来的1.5倍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Test public void testArrayList() {
List<String> names = new ArrayList<>();
names.add("张三"); names.add("李四"); names.add("王五"); names.add("张三"); System.out.println(names);
System.out.println(names.get(1));
names.set(0, "赵六"); System.out.println(names);
names.remove("李四"); System.out.println(names);
names.remove(0); System.out.println(names); }
|
LinkedList —— 链表实现
底层是双向链表,增删快(改指针就行),查询慢(要从头一个一个找)
同时实现了 List 和 Deque(双端队列)接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Test public void testLinkedList() { LinkedList<String> list = new LinkedList<>();
list.add("A"); list.add("B"); list.add("C");
list.addFirst("HEAD"); list.addLast("TAIL"); System.out.println(list);
System.out.println(list.getFirst()); System.out.println(list.getLast());
System.out.println(list.get(2)); }
|
ArrayList vs LinkedList 性能对比
| 操作 |
ArrayList |
LinkedList |
谁赢? |
按下标查询 get(i) |
O(1) 直接算地址 |
O(n) 从头遍历 |
ArrayList |
尾部添加 add(e) |
O(1) 均摊 |
O(1) |
平手 |
中间插入 add(i, e) |
O(n) 要移动元素 |
O(n) 要先找到位置 |
看情况 |
中间删除 remove(i) |
O(n) 要移动元素 |
O(n) 要先找到位置 |
看情况 |
| 头部插入/删除 |
O(n) 所有元素后移 |
O(1) 改指针 |
LinkedList |
| 内存占用 |
较少(连续数组) |
较多(每个节点多两个指针) |
ArrayList |
实战建议:99%的场景用 ArrayList 就对了。LinkedList 只在频繁头部插入删除时才有优势,而且现代CPU的缓存机制让数组的实际表现往往更好
Set:无序不重复
就像数学里的集合概念:元素不能重复,无所谓顺序
HashSet —— 最常用的Set
底层是 Map体系 中的 HashMap(value全放一个固定值),所以性能取决于hash函数
增删查都是 O(1),非常快
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Test public void testHashSet() { Set<String> fruits = new HashSet<>();
fruits.add("苹果"); fruits.add("香蕉"); fruits.add("苹果"); fruits.add("橙子");
System.out.println(fruits); System.out.println(fruits.size());
System.out.println(fruits.contains("苹果"));
fruits.remove("香蕉"); System.out.println(fruits); }
|
TreeSet —— 有序的Set
底层是红黑树,元素会自动排序(自然顺序或自定义Comparator)
增删查都是 O(log n)
1 2 3 4 5 6 7 8 9 10
| @Test public void testTreeSet() { Set<Integer> numbers = new TreeSet<>(); numbers.add(30); numbers.add(10); numbers.add(20); numbers.add(10);
System.out.println(numbers); }
|
LinkedHashSet —— 保留插入顺序的Set
HashSet + 链表维护插入顺序,性能略低于HashSet
1 2 3 4 5 6 7 8 9
| @Test public void testLinkedHashSet() { Set<String> set = new LinkedHashSet<>(); set.add("C"); set.add("A"); set.add("B");
System.out.println(set); }
|
三种Set对比
| Set类型 |
底层 |
有序? |
性能 |
适用场景 |
| HashSet |
HashMap |
无序 |
O(1) 最快 |
去重、判断是否存在 |
| TreeSet |
红黑树 |
自然排序 |
O(log n) |
需要排序的去重 |
| LinkedHashSet |
HashMap+链表 |
插入顺序 |
O(1) 略慢 |
去重且保持顺序 |
Queue:先进先出
像排队买奶茶:先到的先买,后到的排后面(FIFO:First In First Out)
LinkedList 当Queue用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Test public void testQueue() { Queue<String> queue = new LinkedList<>();
queue.offer("第1个顾客"); queue.offer("第2个顾客"); queue.offer("第3个顾客");
System.out.println(queue);
System.out.println(queue.peek());
System.out.println(queue.poll()); System.out.println(queue.poll()); System.out.println(queue); }
|
⚠️ add vs offer、remove vs poll、element vs peek
| 操作 |
抛异常版 |
返回null版(推荐) |
| 入队 |
add() 队满抛异常 |
offer() 队满返回false |
| 出队 |
remove() 队空抛异常 |
poll() 队空返回null |
| 查看队头 |
element() 队空抛异常 |
peek() 队空返回null |
实战建议:用右边那列,别给自己找异常的麻烦
PriorityQueue —— 优先级队列
不是先进先出,而是优先级高的先出(底层是堆)
1 2 3 4 5 6 7 8 9 10 11 12
| @Test public void testPriorityQueue() {
PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.offer(30); pq.offer(10); pq.offer(20);
System.out.println(pq.poll()); System.out.println(pq.poll()); System.out.println(pq.poll()); }
|
常用方法速查
这些方法所有Collection子类都能用:
| 方法 |
作用 |
返回值 |
add(e) |
添加元素 |
boolean |
remove(e) |
删除元素 |
boolean |
contains(e) |
是否包含 |
boolean |
size() |
元素个数 |
int |
isEmpty() |
是否为空 |
boolean |
clear() |
清空 |
void |
iterator() |
获取迭代器 |
Iterator |
toArray() |
转数组 |
Object[] |
遍历方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Test public void testIteration() { List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) { System.out.println(s); }
Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if ("B".equals(s)) { it.remove(); } } System.out.println(list);
list.stream().forEach(System.out::println); }
|
⚠️ 遍历时删除的坑:用for-each遍历时直接调用 list.remove() 会抛 ConcurrentModificationException,必须用 Iterator 的 remove() 方法
集合的交集、并集、差集
对应 集合论基础 中的数学概念,Java用Collection的批量操作实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Test public void testSetOperations() { Set<Integer> setA = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5)); Set<Integer> setB = new HashSet<>(Arrays.asList(3, 4, 5, 6, 7));
Set<Integer> union = new HashSet<>(setA); union.addAll(setB); System.out.println("并集:" + union);
Set<Integer> intersection = new HashSet<>(setA); intersection.retainAll(setB); System.out.println("交集:" + intersection);
Set<Integer> difference = new HashSet<>(setA); difference.removeAll(setB); System.out.println("差集:" + difference); }
|
常见坑
坑1:自定义对象放HashSet/HashMap,不重写hashCode和equals
HashSet判断重复靠 hashCode() + equals(),不重写的话两个”相同”的对象会被当成不同的
详见 Object类 中关于equals和hashCode的约定
坑2:遍历时修改集合
for-each里直接 list.remove() 会炸,要用Iterator
坑3:Arrays.asList() 返回的List不能增删
1 2 3 4 5
| List<String> list = Arrays.asList("A", "B", "C");
List<String> mutableList = new ArrayList<>(Arrays.asList("A", "B", "C")); mutableList.add("D");
|
坑4:List的remove方法重载
1 2 3 4
| List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3)); list.remove(1); list.remove(Integer.valueOf(1));
|
练习题
题1:去重 —— 给定一个List,去掉里面的重复元素,保持原来的顺序
提示:用哪种Set可以保持插入顺序?
1 2 3 4 5 6 7 8
| @Test public void exercise1() { List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3);
Set<Integer> set = new LinkedHashSet<>(list); List<Integer> result = new ArrayList<>(set); System.out.println(result); }
|
题2:统计词频 —— 统计一个字符串数组中每个单词出现的次数
提示:用 Map体系 中的HashMap,key是单词,value是次数
题3:两个List求交集 —— 不用retainAll,自己用HashSet实现
提示:把第一个List放进HashSet,遍历第二个List检查contains