本节摘要:Queue 是先进先出的叫号机,两套方法对应两种失败策略——抛异常或返回特殊值;Deque 双端队列一物两用,既能当队列又能当栈,ArrayDeque 是两用的首选实现。Collections 是集合的百宝箱:排序、反转、最值、不可变集合与同步包装。本节用叫号与栈两个演示讲透两套方法的选择,并过一遍百宝箱里最常用的抽屉。
List 的"有序"是下标意义上的——按下标插到中间叫插队,业务上常常不合规矩。另一类需求严格遵守"先来的先办":消息按序处理、任务按序执行、缓冲区按序吐出。Queue 把这条纪律写进了契约:队尾进、队头出,先进先出。
Queue 的新手坑在于它给每个操作准备了两套方法,失败行为不同。拿"取队头"来说:队空时,element 抛异常、peek 返回 null。两套各有一席之地:
| 操作 | 抛异常版 | 返回特殊值版 | 队空时 |
|---|---|---|---|
| 入队 | add | offer | add 抛异常 offer 返回 false |
| 看队头不移除 | element | peek | element 抛异常 peek 返回 null |
| 取走队头 | remove | poll | remove 抛异常 poll 返回 null |
import java.util.LinkedList; import java.util.Queue; public class QueueDemo { public static void main(String[] args) { Queue<String> window = new LinkedList<>(); // LinkedList 持有的第二张证 window.offer("一号客"); // 队尾入列 window.offer("二号客"); window.offer("三号客"); System.out.println(window.peek()); // 输出:一号客 只看队头 不动它 System.out.println(window.poll()); // 输出:一号客 取走队头 System.out.println(window.poll()); // 输出:二号客 System.out.println(window.size()); // 输出:1 System.out.println(window.poll()); // 输出:三号客 System.out.println(window.peek()); // 输出:null 空队列返回特殊值 不炸 } }
日常更推荐 offer、poll、peek 这一套:空队列是排队的正常状态而不是程序错误,用返回值判断比捕获异常体面得多——异常是昂贵的控制流(第 7 章会展开这个原则)。队列还有"双端队列 Deque"的加强版:两头都能进出,于是同一副结构既能当队列(一端进另一端出)又能当栈(同一端进出):
import java.util.ArrayDeque; import java.util.Deque; public class DequeDemo { public static void main(String[] args) { Deque<String> stack = new ArrayDeque<>(); // 当栈用:后进先出 stack.push("第一块盘"); // push 入栈(栈顶) stack.push("第二块盘"); stack.push("第三块盘"); System.out.println(stack.pop()); // 输出:第三块盘 后进先出 System.out.println(stack.peek()); // 输出:第二块盘 看栈顶不出栈 Deque<String> queue2 = new ArrayDeque<>(); // 同一个类 也能当队列 queue2.addLast("甲"); // 队尾进 queue2.addLast("乙"); System.out.println(queue2.removeFirst()); // 输出:甲 队头出 先进先出 } }
这里有一条工程经验要立住:旧的 Stack 类已经不推荐使用——它基于向量、方法全同步,单线程白白付锁开销,而且它按"继承"扩展出了一批不合队词语义的方法。需要栈就用 ArrayDeque(或 LinkedList),两者性能相当,ArrayDeque 略优。LinkedList 的两张证(List 与 Queue)在 5.1 节谱系图里出现过,ArrayDeque 则是双端专用实现。
Collections(带 s,注意与 Collection 接口区分)是集合版的工具窗口(4.3 节那种全静态构造),常用的抽屉大致五类:
import java.util.ArrayList; import java.util.Collections; import java.util.List; public class KitDemo { public static void main(String[] args) { List<Integer> nums = new ArrayList<>(List.of(42, 7, 19, 73, 25)); Collections.sort(nums); // 一:排序 自然序 System.out.println(nums); // 输出:[7, 19, 25, 42, 73] Collections.reverse(nums); // 二:反转 System.out.println(nums); // 输出:[73, 42, 25, 19, 7] System.out.println(Collections.max(nums)); // 三:最值 输出:73 System.out.println(Collections.min(nums)); // 输出:7 Collections.swap(nums, 0, 1); // 换位 System.out.println(nums); // 输出:[42, 73, 25, 19, 7] Collections.shuffle(nums); // 打乱 模拟随机分组用 System.out.println("打乱后顺序随机:" + nums); List<String> fixed = List.of("登记", "变更", "注销"); // 四:不可变集合 // fixed.set(0, "作废"); // 运行时抛 UnsupportedOperationException System.out.println(fixed); // 输出:[登记, 变更, 注销] List<Integer> oldStyle = new ArrayList<>(); // 五:同步包装 老代码兼容 List<Integer> synced = Collections.synchronizedList(oldStyle); System.out.println(synced.getClass().getSimpleName()); // 输出包装类名 含 Synchronized } }
五类抽屉各有讲究。排序:默认自然序(元素实现 Comparable),传比较器可自定义(5.2 节两种比较方式在这里通用);排序只对 List 有意义——Set 与 Map 里 HashSet 本身无序,TreeSet 是构造时定序,不需要事后排。不可变集合:List.of、Set.of、Map.of 这些工厂方法(以及包装既有集合的方法)产出只读视图,改它抛不支持操作异常——把配置、字典、枚举表做成不可变集合,能杜绝"谁顺手改了公共字典"这类事故,与 4.2 节 String 不可变的思路同源。同步包装:把普通集合包成方法级同步的版本,历史上是集合并发安全的快速补丁,但方法级同步粒度粗、复合操作(先查后改)仍不安全,现代代码更倾向使用并发集合——那属于第 8 章之后的进阶话题,这里知道"有这个抽屉、别乱用"即可。
完整案例:背景:营业厅取号系统,客人陆续取号、柜员按号办理,午休时暂停办理、统计当前等候人数与下一个办理者。操作:用 ArrayDeque 做号池,offerLast 入队、peekFirst 看下一位、pollFirst 办结:
import java.util.ArrayDeque; import java.util.Deque; public class TicketCase { public static void main(String[] args) { Deque<String> line = new ArrayDeque<>(); line.offerLast("A001"); line.offerLast("A002"); line.offerLast("A003"); System.out.println("等候人数:" + line.size()); // 输出:等候人数:3 System.out.println("下一位:" + line.peekFirst()); // 输出:下一位:A001 line.pollFirst(); // 叫号办理 A001 line.pollFirst(); // 叫号办理 A002 System.out.println("办理两位后等候:" + line.size()); // 输出:办理两位后等候:1 System.out.println("下一位:" + line.peekFirst()); // 输出:下一位:A003 while (!line.isEmpty()) line.pollFirst(); // 清场 System.out.println("空场时下一位:" + line.peekFirst()); // 输出:空场时下一位:null } }
结果:入队、查看、办理、清场全程只用三个方法。解读:offer 与 peek 在空队列上安静地返回特殊值,使清场循环不需要额外的异常处理——两套方法选 offer 系的价值正在于此。变式:若要求"VIP 插到队头",用双端的 offerFirst;若要按优先级叫号(VIP 先于普通号,同级别先来先办),普通队列不够用,需要优先级队列——它基于堆结构,入队出队都是对数级,5.2 节的比较规则原样适用;再往后的"任务定时延迟执行"则是它的变体,这些在并发章节会再遇到。
集合的货都有了,还差最后一道保险:往 List 里装什么、装进去之后取出来是什么类型——没有类型栏的年代全靠强制转换,一错就崩。下一节讲泛型,登记表上的这道类型栏。