QUICKREF · IO · NIO · HTTP CLIENT
IO、NIO 与 HttpClient
JDK 21 · 速查主题 09 / 12 · 预计 26 分钟
一句话日常 IO 的 90% 用 Files 就能完成:readString / writeString / newBufferedReader / lines / walk。需要高并发网络请求时用 HttpClient;只有大文件、零拷贝或内存映射才需要下沉到 Channel 与 ByteBuffer。
学完你要能自己回答
- Files.readString、Files.lines、Files.walk 分别有什么资源释放要求?
- HttpClient 是线程安全的吗?同步与异步调用分别适合什么场景?
- 为什么文件编码要显式指定?JDK 18 之后默认编码发生了什么变化?
Path 与 Files:先掌握这 12 个方法01
| 方法 | 作用 | 注意 |
|---|---|---|
Path.of("a", "b.txt") | 构造路径 | 跨平台分隔符由 API 处理 |
resolve / resolveSibling | 拼接子路径 / 兄弟路径 | 绝对路径会直接替换 |
relativize | 求相对路径 | 两者需同根 |
normalize | 消除 . 与 .. | 不访问文件系统 |
toAbsolutePath | 转绝对路径 | — |
Files.exists / notExists | 存在性判断 | 检查后使用仍有竞态 |
Files.createDirectories | 递归建目录 | 父目录已存在不报错 |
Files.copy / move | 复制 / 移动 | 默认不覆盖,需 REPLACE_EXISTING |
Files.delete / deleteIfExists | 删除 | delete 不存在会抛异常 |
Files.size | 文件大小 | 目录大小不可靠 |
Files.mismatch | 比较两个文件 | JDK 12+,返回首个不同位置 |
Files.probeContentType | 探测 MIME | 依赖平台,可能为 null |
Path dir = Path.of("data", "2026");
Files.createDirectories(dir); // 父目录不存在也能建
Path file = dir.resolve("users.txt");
Path abs = file.toAbsolutePath().normalize();
System.out.println(abs);
Path base = Path.of("data");
System.out.println(base.relativize(file)); // 2026/users.txt
Path src = Path.of("a.txt");
Path dst = Path.of("backup", "a.txt");
Files.createDirectories(dst.getParent());
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(dst, Path.of("backup", "b.txt"), StandardCopyOption.ATOMIC_MOVE);
Files.deleteIfExists(Path.of("backup", "b.txt"));
Files.exists 之后再创建文件存在 TOCTOU 竞态。需要「不存在才创建」时用 Files.createFile 并捕获 FileAlreadyExistsException,或用 CREATE_NEW 打开选项。文本读写:优先用 Files 一行搞定02
JDK 11 的 readString / writeString 覆盖了大多数小文件场景。大文件不要 readAllBytes,改用缓冲读取或流式处理。
Path path = Path.of("data", "notes.txt");
// 小文件:一行读写
Files.writeString(path, "第一行\n第二行\n", StandardCharsets.UTF_8);
String all = Files.readString(path, StandardCharsets.UTF_8);
System.out.println(all);
// 按行读取(自动关闭)
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// 流式处理(必须关闭)
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
long count = lines.filter(s -> !s.isBlank()).count();
System.out.println(count);
}
Files.lines返回的 Stream 持有文件句柄,必须 try-with-resources。Files.walk/Files.list同理,必须关闭。readAllLines一次性加载全部行,只适合小文件。- 写文件默认截断;追加用
StandardOpenOption.APPEND。 Files.writeString默认 CREATE + TRUNCATE_EXISTING + WRITE。
try (Stream<Path> paths = Files.walk(Path.of("src"))) {
List<Path> javaFiles = paths
.filter(p -> p.toString().endsWith(".java"))
.toList();
System.out.println(javaFiles.size());
}
JDK 18 起 file.encoding 默认 UTF-8(JEP 400),但不要依赖平台默认值:显式传 StandardCharsets.UTF_8,代码在旧 JDK 或其他平台上也一致。
HttpClient:同步与异步03
JDK 11 标准化的 java.net.http.HttpClient 支持 HTTP/1.1、HTTP/2、WebSocket,且是线程安全、可复用的。不要为每个请求 new 一个客户端。
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
HttpRequest request = HttpRequest.newBuilder(
URI.create("https://httpbin.org/get?q=jdk21"))
.timeout(Duration.ofSeconds(10))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.exceptionally(ex -> {
System.err.println("请求失败: " + ex.getMessage());
return null;
});
// 组合多个请求
CompletableFuture<String> a = client.sendAsync(reqA, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
CompletableFuture<String> b = client.sendAsync(reqB, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
String merged = a.thenCombine(b, (x, y) -> x + y).join();
send阻塞当前线程;sendAsync返回 CompletableFuture,底层使用客户端执行器。- 请求体用
BodyPublishers.ofString/ofFile/ofByteArray。 - 响应体用
BodyHandlers.ofString/ofFile/ofLines/discarding。 - HTTP/3 在 JDK 26 才进入标准 HTTP Client(JEP 517);JDK 21 只能到 HTTP/2。
- 生产环境要设置连接超时、请求超时和重试策略。
// 带查询参数的 URL 需要编码
String q = "jdk 21";
String url = "https://example.com/search?q=" +
URLEncoder.encode(q, StandardCharsets.UTF_8);
// POST JSON
HttpRequest post = HttpRequest.newBuilder(URI.create("https://example.com/api"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"jdk\"}"))
.build();
NIO:什么时候需要下沉04
大多数业务不需要直接操作 ByteBuffer。只有大文件、内存映射、零拷贝、非阻塞网络等场景才值得增加复杂度。
// FileChannel + ByteBuffer 读取
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(4096);
while (channel.read(buffer) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
byte b = buffer.get();
// 处理字节
}
buffer.clear();
}
}
// 内存映射:适合超大文件随机访问
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
MappedByteBuffer mapped = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size());
byte first = mapped.get(0);
System.out.println((char) first);
}
ByteBuffer的 flip / clear / rewind 是新手最容易搞混的三兄弟。- 直接缓冲区(DirectBuffer)分配成本高,受
-XX:MaxDirectMemorySize限制,泄漏时表现为 OOM: Direct buffer memory。 - 内存映射文件释放依赖 GC / Cleaner,长期持有会占用虚拟内存。
Files.readAllBytes对大文件会直接吃满堆内存。
try-with-resources 与异常05
IO 资源必须关闭。try-with-resources 按声明的逆序关闭;主异常与关闭异常同时发生时,关闭异常作为 suppressed 附加在主异常上。
try (InputStream in = Files.newInputStream(src);
OutputStream out = Files.newOutputStream(dst)) {
in.transferTo(out);
} catch (IOException e) {
for (Throwable suppressed : e.getSuppressed()) {
System.err.println("关闭时异常: " + suppressed);
}
throw e;
}
- 实现
AutoCloseable的对象都可以放进 try-with-resources。 - 不要手动在 finally 里关闭多个资源而不处理关闭异常;try-with-resources 会正确合并。
InputStream.transferTo是 JDK 9+ 的便捷拷贝方法,但不会自动缓冲;大文件可配合BufferedInputStream。- 网络请求失败时区分连接超时、读取超时、非 2xx 状态码,不要统一吞掉。
处理用户提供的路径时注意路径穿越:
resolve(userInput).normalize() 后必须校验仍在允许的根目录下,不能直接拼接。版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| HttpClient | 11 正式 | JDK 9 孵化,11 标准化;支持 HTTP/2。 |
| Files.readString / writeString | 11 正式 | 小文件读写首选。 |
| Files.mismatch | 12 正式 | 比较文件内容。 |
| 默认 UTF-8 | 18 正式 | JEP 400;仍建议显式指定字符集。 |
| HTTP/3 for HttpClient | 26 正式 | JDK 21 不支持。 |
| 虚拟线程友好的阻塞 IO | 21 正式 | 阻塞 IO 会自动卸载虚拟线程(pinning 情况除外)。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。
01. Files.lines 和 Files.readAllLines 的资源管理有什么区别?
readAllLines 一次性把全部行读入内存并关闭文件,适合小文件;Files.lines 返回一个持有文件句柄的惰性 Stream,必须用 try-with-resources 关闭,适合大文件流式处理。
02. HttpClient 应该复用还是每次新建?为什么?
应该复用。HttpClient 是线程安全且不可变的,内部维护连接池和执行器;每次新建会丢失连接复用、增加握手开销,并可能耗尽资源。
03. JDK 18 默认 UTF-8 之后,还需要显式指定字符集吗?
需要。默认值受 JVM 参数和平台影响,显式指定 StandardCharsets.UTF_8 可以保证跨 JDK 版本、跨平台行为一致,也避免依赖隐式配置。
04. 什么情况下才需要直接用 ByteBuffer / FileChannel?
需要处理超大文件、内存映射、零拷贝、非阻塞网络或精细控制缓冲区时。普通文本/小文件读写优先用 Files 与 BufferedReader,代码更短、更安全。
学习进度
记录保存在本机浏览器 · 不上传