适用读者:会用
ArrayList/HashMap但被问"Vector为什么不用了"就卡住的人。
前置:S04-J01(List/迭代器)、J02(HashMap)、J06(IO/NIO)。
覆盖旧文:2749–2761(集合框架概述、List/ArrayList/LinkedList/Set/Map/Queue、Vector/Stack、I/O 流概述、字节流/字符流/文件操作)+2993–3005(同题重复)+4552(HashMap 原理)→ 27 篇 / 约 22 万字符。
扩展点:E-S04-030(集合选型决策表)、E-S04-031(Vector/Stack废弃的真正原因)、E-S04-032(字节流↔字符流的编码桥接与乱码定位)。
上一篇:S04-J09 · 异常体系:受检与否、finally的四种失效与资源关闭
下一篇:补编第 5 篇 · 反射、注解、泛型与序列化(待写)。
1. 一句话结论
集合的选择只看三件事:要不要顺序、要不要唯一、要不要排序。 而 IO 的所有乱码问题,都出在"字节与字符之间那一次编码转换发生在哪里、用的是什么字符集"。
出处:
A-2749–A-2761(集合与 IO 各章)、A-2993–A-3005(同题重复)、A-4552(HashMap 原理,与 J02 同题)。
2. 集合骨架与选型(E-S04-030)
Iterable
└── Collection
├── List 有序、可重复、可按索引
├── Set 不重复
└── Queue 队列语义(FIFO / 优先级)
Map(独立分支,不算 Collection) 键值对、键唯一
| 需求 | 首选 | 不要用 | 理由 |
|---|---|---|---|
| 随机访问为主 | ArrayList |
LinkedList |
数组连续内存,缓存友好 |
| 频繁头尾增删 | ArrayDeque |
LinkedList/Stack |
环形数组,无节点开销 |
| 去重 + 保插入序 | LinkedHashSet |
HashSet |
后者顺序不定 |
| 排序集合/范围查询 | TreeSet / TreeMap |
HashSet |
红黑树 O(log n) |
| 键值、O(1) 查找 | HashMap |
Hashtable |
后者全表锁 |
| 并发读多写少 | ConcurrentHashMap |
Collections.synchronizedMap |
分段/CAS,粒度细 |
| 单线程栈 | ArrayDeque |
Stack |
Stack 继承 Vector,全同步 |
判据:"需要线程安全"不是选 Vector 的理由——它是单锁全同步,吞吐远低于 ConcurrentHashMap/CopyOnWriteArrayList。
2.1 Vector/Stack 废弃的真正原因(E-S04-031)
① 每个方法都 synchronized(含 size()),无谓竞争
② 依赖"外部同步的迭代器":遍历仍需手动加锁,否则 CME
③ Stack 继承 Vector —— 组合优于继承的反面教材(栈语义被 List 方法污染)
④ 已被 ArrayDeque(栈/队列)与 CopyOnWriteArrayList(并发读多)取代
版本边界:Stack 未被标记 @Deprecated(仍可用、不推荐);"废弃"是工程共识,不是 API 标记——这点在面试里说清很加分。
2.2 Queue 的两套语义(高频追问)
| 操作 | 失败抛异常 | 失败返回特殊值 |
|---|---|---|
| 插入 | add(e) |
offer(e) → false |
| 删除队头 | remove() |
poll() → null |
| 查看队头 | element() |
peek() → null |
选型:生产代码优先用 offer/poll/peek——容量受限队列(ArrayBlockingQueue)用 add 会直接抛 IllegalStateException,offer 可让调用方决定降级策略。
| 实现 | 结构 | 边界 |
|---|---|---|
ArrayDeque |
环形数组 | 不允许 null;push/pop 当栈用 |
PriorityQueue |
二叉堆 | 不保证遍历有序,只保证 poll 出最小/最大 |
ArrayBlockingQueue |
有界数组 + 锁 | 生产/消费限流首选 |
LinkedBlockingQueue |
链表(默认 Integer.MAX_VALUE) |
无界风险 → 线程池 OOM(见 S25-J07) |
2.3 与 J02 的重复比对(A-4552 vs A-J02)
| 点 | 旧文 4552 说法 |
本系列结论 |
|---|---|---|
| 扩容 | "容量翻倍" | 翻倍 + 高低位拆分(hash & oldCap),避免重新哈希(J02 §4) |
| 树化 | "链表过长转红黑树" | 阈值 8 + 容量 ≥ 64,否则先扩容(J02 §4) |
| 线程安全 | "不安全" | 并发 put 可能丢数据/死循环(JDK 8 已修死循环,仍丢数据) |
处置:4552 全文标 duplicate_ref → S04-J02,不重复成文。
3. IO:四种流与编码桥接(E-S04-032)
字节流(8 bit):InputStream / OutputStream
字符流(16 bit char):Reader / Writer
桥接:InputStreamReader(InputStream, Charset) / OutputStreamWriter
缓冲:BufferedInputStream / BufferedReader(默认 8 KB / 8 KB)
| 问题 | 答 |
|---|---|
| 什么时候用字符流 | 处理文本(有编码语义);二进制(图片/压缩包)必须字节流 |
| 乱码三大来源 | ① 写入与读取字符集不一致 ② 未指定字符集(用平台默认)③ 字节被截断(用 read() 单字节丢多字节字符) |
FileReader 能指定编码吗 |
不能(用平台默认);要指定必须 new InputStreamReader(new FileInputStream(f), UTF_8) |
read() 与 read(byte[]) |
前者返回单个字节/字符,后者返回实际读取数(可能 < 数组长度)——循环必须用返回值 |
坑 1(最经典):
int n; byte[] buf = new byte[1024];
while ((n = in.read(buf)) != -1) { out.write(buf, 0, n); } // ✅ 必须写 0,n
// 错法:out.write(buf); → 末尾填充无效字节,文件变大且损坏
3.1 Files 工具类 vs 传统流
// JDK 7+(推荐)
String s = Files.readString(Path.of("a.txt"), StandardCharsets.UTF_8); // JDK 11+
List<String> lines = Files.readAllLines(Path.of("a.txt"), UTF_8);
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
try (var stream = Files.lines(Path.of("big.log"))) { stream.filter(...).count(); } // 惰性、流式
| 场景 | 用什么 |
|---|---|
| 小文件整读 | Files.readString/readAllLines |
| 大文件 | Files.lines + 流式处理,或 BufferedReader 循环 |
| 复制/移动 | Files.copy/move(可指定 REPLACE_EXISTING、COPY_ATTRIBUTES) |
| 临时文件 | Files.createTempFile(自动随机名,避免竞态) |
| 遍历目录 | Files.walk(记得 try-with-resources,它持有目录句柄) |
判据:Files.lines 与 Files.walk 都返回需关闭的资源——忘了关就是文件句柄泄漏(Linux 下表现为 Too many open files)。
3.2 四种 IO 模型的一句话对照(与 J06 呼应)
| 模型 | 阻塞点 | 典型 API |
|---|---|---|
| 阻塞 IO | read 等数据 |
InputStream |
| 非阻塞 IO | 立即返回,需轮询 | Channel + configureBlocking(false) |
| IO 多路复用 | select 等就绪 |
Selector |
| 异步 IO | 回调完成 | AsynchronousChannelGroup(Linux 上仍靠线程池模拟) |
4. 对照实验:Vector vs ArrayList vs CopyOnWriteArrayList
// JDK 17,单机 8 核;4 线程 × 100 万次 add(量级示意,非严格 JMH)
Vector :全方法同步,线程安全但吞吐最低
Collections.synchronizedList(new ArrayList<>()) :等价于 Vector 的锁粒度
CopyOnWriteArrayList :写时复制,10 万次以上写入极慢(每次 O(n) 复制)
ArrayList(单线程) :最快,但并发写会丢数据/数组越界
口径:上表为机制推断(依据 synchronized 全方法与写时复制语义)。要发表结论请自行 JMH 测量——面试说"量级与原因"比给精确倍数安全。
5. 反例:六个高频错用
| 写法 | 问题 |
|---|---|
new Stack<>() 当栈 |
全同步 + 语义污染,改 ArrayDeque |
LinkedList 做随机访问 |
get(i) 是 O(n),链表遍历 |
PriorityQueue 遍历取有序 |
只有 poll 有序 |
new LinkedBlockingQueue<>() 无参 |
容量 Integer.MAX_VALUE → 堆积 OOM |
out.write(buf) |
写出填充字节,文件损坏 |
new FileReader(f) 读 UTF-8 |
平台默认编码,跨机器乱码 |
6. 自测
- 集合选型的三问?并发场景为什么不用
Vector? Queue两套语义各自的方法?生产代码优先哪套?FileReader能指定字符集吗?如何指定?read(byte[])的返回值为什么要用于write的第三个参数?Files.lines/Files.walk为什么要 try-with-resources?
答案
- 顺序/唯一/排序;
Vector全方法单锁,吞吐低且迭代仍需外部加锁。 add/remove/element与offer/poll/peek;优先后者。- 不能;用
InputStreamReader+Charset。 - 返回实际读取字节数,末尾可能是上次残留的无效字节。
- 它们持有文件/目录句柄,不关闭会泄漏。
7. 下一篇
补编第 5 篇 · 反射、注解、泛型与序列化 —— 运行时的四把钥匙:Class/Method 的性能与安全代价、注解的保留策略与处理器、泛型擦除的补救(TypeToken)、Serializable 为何被逐步弃用。
本篇属于系列《S04 · Java 语言核心》。旧文来源:A-2749–2761、A-2993–3005、A-4552;扩展点 E-S04-030/031/032。对照实验数字为机制推断,未经 JMH 实测。