JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · IO · NIO · HTTP CLIENT

IO、NIO 与 HttpClient

JDK 21 · 速查主题 09 / 12 · 预计 26 分钟
核心26 分钟JEP 321JEP 400
一句话日常 IO 的 90% 用 Files 就能完成:readString / writeString / newBufferedReader / lines / walk。需要高并发网络请求时用 HttpClient;只有大文件、零拷贝或内存映射才需要下沉到 Channel 与 ByteBuffer。

学完你要能自己回答

  1. Files.readString、Files.lines、Files.walk 分别有什么资源释放要求?
  2. HttpClient 是线程安全的吗?同步与异步调用分别适合什么场景?
  3. 为什么文件编码要显式指定?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状态说明
HttpClient11 正式JDK 9 孵化,11 标准化;支持 HTTP/2。
Files.readString / writeString11 正式小文件读写首选。
Files.mismatch12 正式比较文件内容。
默认 UTF-818 正式JEP 400;仍建议显式指定字符集。
HTTP/3 for HttpClient26 正式JDK 21 不支持。
虚拟线程友好的阻塞 IO21 正式阻塞 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,代码更短、更安全。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。