2051 字
约 6 分钟
7
S04-J10 · 集合与 IO 补编:废弃的 Vector/Stack、Queue 两套语义与流的桥接

适用读者:会用 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. 自测

  1. 集合选型的三问?并发场景为什么不用 Vector?
  2. Queue 两套语义各自的方法?生产代码优先哪套?
  3. FileReader 能指定字符集吗?如何指定?
  4. read(byte[]) 的返回值为什么要用于 write 的第三个参数?
  5. Files.lines/Files.walk 为什么要 try-with-resources?
答案
  1. 顺序/唯一/排序;Vector 全方法单锁,吞吐低且迭代仍需外部加锁。
  2. add/remove/element 与 offer/poll/peek;优先后者。
  3. 不能;用 InputStreamReader + Charset。
  4. 返回实际读取字节数,末尾可能是上次残留的无效字节。
  5. 它们持有文件/目录句柄,不关闭会泄漏。

7. 下一篇

补编第 5 篇 · 反射、注解、泛型与序列化 —— 运行时的四把钥匙:Class/Method 的性能与安全代价、注解的保留策略与处理器、泛型擦除的补救(TypeToken)、Serializable 为何被逐步弃用。


本篇属于系列《S04 · Java 语言核心》。旧文来源:A-2749–2761、A-2993–3005、A-4552;扩展点 E-S04-030/031/032。对照实验数字为机制推断,未经 JMH 实测。

S04-J10 · 集合与 IO 补编:废弃的 Vector/Stack、Queue 两套语义与流的桥接
https://www.clxhxhhr.top/posts/5233/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。