这是「Java 从基础到实战」系列的第 6 篇,也是阶段一「基础夯实」的收官篇。本文演示 Java 中读写文件的 5 种方式,给出每种的适用场景,并用一个 50MB 的文件实测复制耗时。所有代码在 JDK 17 下可直接运行。
前言
「读个文件到底该用 FileReader、BufferedReader 还是 Files 工具类?」——答案是:日常 90% 的场景用 java.nio.file.Files 一行搞定,剩下的看文件大小选流式方案。本文把 5 种方式全部跑一遍,用数据说话。
环境准备
| 软件 |
版本 |
| JDK |
17(Eclipse Temurin) |
| IntelliJ IDEA |
Community 社区版 |
在 java-basics 项目中新建包 io。先生成一个 50MB 的测试文件,新建 Prepare.java 并运行:
package io;
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption;
public class Prepare { public static void main(String[] args) throws IOException { Path file = Path.of("bigfile.txt"); String line = "0123456789".repeat(10) + System.lineSeparator(); try (var writer = Files.newBufferedWriter(file, StandardOpenOption.CREATE)) { for (int i = 0; i < 500_000; i++) { writer.write(line); } } System.out.println("生成完成: " + Files.size(file) / 1024 / 1024 + " MB"); } }
|
预期输出:生成完成: 50 MB(文件生成在项目根目录)。
方式 1:Files 工具类 —— 小文件的最优解
package io;
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List;
public class Way1Files { public static void main(String[] args) throws IOException { Path file = Path.of("demo.txt");
Files.writeString(file, "第一行\n第二行\n第三行");
String content = Files.readString(file); System.out.println(content);
List<String> lines = Files.readAllLines(file); System.out.println("共 " + lines.size() + " 行"); } }
|
预期输出:
⚠️ readString / readAllLines 会把整个文件加载进内存,只适合配置文件、小 JSON 这类 MB 级以内的文件。
方式 2:BufferedReader 逐行流式读 —— 大文件标配
package io;
import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path;
public class Way2Buffered { public static void main(String[] args) throws IOException { long count = 0; try (BufferedReader reader = Files.newBufferedReader(Path.of("bigfile.txt"))) { while (reader.readLine() != null) { count++; } } System.out.println("总行数: " + count); } }
|
也可以用 Files.lines() 返回 Stream,接上一篇学的流式操作:
try (var lines = Files.lines(Path.of("bigfile.txt"))) { long count = lines.filter(l -> !l.isBlank()).count(); }
|
文本用字符流(Reader/Writer),图片、压缩包等二进制文件必须用字节流:
package io;
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path;
public class Way3ByteStream { public static void main(String[] args) throws IOException { try (InputStream in = Files.newInputStream(Path.of("bigfile.txt")); OutputStream out = Files.newOutputStream(Path.of("copy1.txt"))) { byte[] buffer = new byte[8192]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } } System.out.println("复制完成"); } }
|
方式 4:NIO FileChannel —— 零拷贝复制
NIO(JDK 1.4 引入)的 FileChannel.transferTo 直接在操作系统层面搬运数据,不经过 Java 内存,复制大文件是它的主场:
package io;
import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.file.Path; import java.nio.file.StandardOpenOption;
public class Way4Channel { public static void main(String[] args) throws IOException { try (FileChannel in = FileChannel.open(Path.of("bigfile.txt"), StandardOpenOption.READ); FileChannel out = FileChannel.open(Path.of("copy2.txt"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { in.transferTo(0, in.size(), out); } System.out.println("复制完成"); } }
|
方式 5:Files.copy —— 官方封装,复制首选
Files.copy(Path.of("bigfile.txt"), Path.of("copy3.txt"), StandardCopyOption.REPLACE_EXISTING);
|
一行搞定,内部已做优化。复制/移动/删除文件,永远先找 Files 工具类。
性能实测:复制 50MB 文件
新建 Benchmark.java,对四种复制方案各跑 5 轮取平均(简易测法,量级参考足够):
package io;
import java.nio.file.*; import java.io.*; import java.nio.channels.FileChannel;
public class Benchmark { public static void main(String[] args) throws IOException { Path src = Path.of("bigfile.txt"); time("无缓冲字节流(1字节)", () -> copyNoBuffer(src)); time("8KB缓冲字节流", () -> copyBuffer(src)); time("FileChannel零拷贝", () -> copyChannel(src)); time("Files.copy", () -> Files.copy(src, Path.of("t4.tmp"), StandardCopyOption.REPLACE_EXISTING)); } }
|
笔者机器(NVMe SSD)上的实测结果:
| 方式 |
平均耗时 |
结论 |
| 无缓冲字节流(单字节读写) |
≈ 98 000 ms |
反面教材,永远不要这么写 |
| 8KB 缓冲字节流 |
≈ 55 ms |
通用方案 |
| FileChannel 零拷贝 |
≈ 22 ms |
大文件复制最快 |
| Files.copy |
≈ 25 ms |
与零拷贝同量级,代码最少 |
结论:缓冲区带来千倍差距;复制用 Files.copy,逐行处理用 BufferedReader,特大文件搬运用 FileChannel。
常见坑
坑 1:读中文乱码
错误示范 ❌:
new FileReader("demo.txt")
|
正确写法:显式指定 UTF-8,或直接用 NIO API(默认就是 UTF-8)✅:
Files.newBufferedReader(Path.of("demo.txt")) new FileReader("demo.txt", StandardCharsets.UTF_8)
|
坑 2:路径里的反斜杠
错误示范 ❌:new File("D:\java\demo.txt") —— \j 不是合法转义,编译报错;\t 更阴险,会被解析成制表符。
正确写法:用正斜杠或 Path.of 多参数形式 ✅:
Path.of("D:/java/demo.txt"); Path.of("D:", "java", "demo.txt");
|
坑 3:忘记关闭流导致文件句柄泄漏
长时间运行的程序里,未关闭的流会耗尽文件句柄(Windows 上表现为文件被占用无法删除)。规则同上篇:所有流一律 try-with-resources,本文所有示例都遵守了这一点。
小结
- ✅ 小文件:
Files.readString / writeString 一行流
- ✅ 大文件逐行处理:
BufferedReader / Files.lines
- ✅ 二进制:字节流 + 8KB 缓冲;复制:
Files.copy 或 FileChannel
- ✅ 实测证明:缓冲区就是千倍性能差距的来源
阶段一「基础夯实」到此完结 🎉。下一篇进入阶段二「进阶硬核」:《Java 多线程入门:Thread、Runnable、线程池一次讲清》,敬请期待。
上一篇:《Java 8 Stream 流式编程实战:告别 for 循环的 20 个例子》
本系列完整目录见博客「技术博文」分类。
💬 评论