17 - I/O:字节流、字符流、NIO、Files、序列化
17 - I/O:字节流、字符流、NIO、Files、序列化
I/O 体系概览
字节流(byte):InputStream / OutputStream
├── FileInputStream / FileOutputStream — 文件
├── ByteArrayInputStream / ByteArrayOutputStream — 内存
├── BufferedInputStream / BufferedOutputStream — 缓冲
├── DataInputStream / DataOutputStream — 基本类型
└── ObjectInputStream / ObjectOutputStream — 序列化
字符流(char):Reader / Writer
├── FileReader / FileWriter — 文件
├── BufferedReader / BufferedWriter — 缓冲
├── StringReader / StringWriter — 内存
├── InputStreamReader / OutputStreamWriter — 字节→字符桥接
└── PrintWriter / Scanner — 格式化
字节流
import java.io.*;
public class ByteStreamDemo {
// 文件复制(字节流)
public static void copyFile(String src, String dest) throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream(src));
OutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
}
// JDK 9+ 简化版
public static void copyFileNIO(String src, String dest) throws IOException {
try (var in = new FileInputStream(src); var out = new FileOutputStream(dest)) {
in.transferTo(out);
}
}
// 内存字节流
public static byte[] readAllBytes(InputStream in) throws IOException {
try (var baos = new ByteArrayOutputStream()) {
in.transferTo(baos);
return baos.toByteArray();
}
}
// DataInputStream 读写基本类型
public static void writeData(String path) throws IOException {
try (var dos = new DataOutputStream(new FileOutputStream(path))) {
dos.writeInt(42);
dos.writeDouble(3.14);
dos.writeUTF("Hello");
}
try (var dis = new DataInputStream(new FileInputStream(path))) {
System.out.println(dis.readInt());
System.out.println(dis.readDouble());
System.out.println(dis.readUTF());
}
}
public static void main(String[] args) throws IOException {
writeData("/tmp/data.bin");
}
}
字符流
import java.io.*;
public class CharStreamDemo {
// 读取文本文件
public static List<String> readLines(String path) throws IOException {
try (var reader = new BufferedReader(new FileReader(path))) {
return reader.lines().toList();
}
}
// 写入文本文件
public static void writeText(String path, String content) throws IOException {
try (var writer = new BufferedWriter(new FileWriter(path))) {
writer.write(content);
}
}
// 追加写入
public static void appendText(String path, String content) throws IOException {
try (var writer = new BufferedWriter(new FileWriter(path, true))) {
writer.write(content);
writer.newLine();
}
}
// 编码转换
public static String readFileWithEncoding(String path, String charset) throws IOException {
try (var reader = new BufferedReader(
new InputStreamReader(new FileInputStream(path), charset))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
public static void main(String[] args) throws IOException {
writeText("/tmp/test.txt", "Hello\nWorld\nJava");
var lines = readLines("/tmp/test.txt");
lines.forEach(System.out::println);
}
}
NIO.2 — Path 和 Files(JDK 7+)
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
public class NIOTwoDemo {
public static void main(String[] args) throws Exception {
Path path = Path.of("/tmp", "test.txt");
// ---- Files 工具类 ----
// 写入(一行代码)
Files.writeString(path, "Hello\nWorld\nJava", StandardCharsets.UTF_8);
// 读取
String content = Files.readString(path);
System.out.println(content);
// 按行读取
List<String> lines = Files.readAllLines(path);
Stream<String> lineStream = Files.lines(path);
// 读取所有字节
byte[] bytes = Files.readAllBytes(path);
// 创建目录
Path dir = Path.of("/tmp/demo");
Files.createDirectories(dir);
// 复制文件
Files.copy(path, dir.resolve("copy.txt"), StandardCopyOption.REPLACE_EXISTING);
// 移动/重命名
// Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
// 删除(必须存在)
Files.deleteIfExists(dir.resolve("copy.txt"));
// ---- 文件属性 ----
System.out.println("大小: " + Files.size(path));
System.out.println("最后修改: " + Files.getLastModifiedTime(path));
System.out.println("是否存在: " + Files.exists(path));
System.out.println("是否目录: " + Files.isDirectory(dir));
System.out.println("是否常规文件: " + Files.isRegularFile(path));
// ---- 遍历目录 ----
System.out.println("\n/tmp 目录内容:");
try (Stream<Path> paths = Files.list(Path.of("/tmp"))) {
paths.limit(10).forEach(p ->
System.out.printf(" %s %s%n", Files.isDirectory(p) ? "📁" : "📄", p.getFileName()));
}
// 递归遍历
System.out.println("\n递归遍历 /tmp/demo:");
Files.walk(dir, 3).forEach(p -> System.out.println(" " + p));
// 查找文件
try (Stream<Path> found = Files.find(Path.of("/tmp"), 5,
(p, attr) -> p.toString().endsWith(".txt") && attr.isRegularFile())) {
found.forEach(System.out::println);
}
// ---- Path 操作 ----
Path p = Path.of("/home/user/docs/file.txt");
System.out.println("文件名: " + p.getFileName());
System.out.println("父目录: " + p.getParent());
System.out.println("名称数量: " + p.getNameCount());
System.out.println("绝对路径: " + p.toAbsolutePath());
System.out.println("后缀: " + p.getFileName().toString()
.substring(p.getFileName().toString().lastIndexOf('.')));
// 路径拼接
Path base = Path.of("/home/user");
Path resolved = base.resolve("docs/file.txt"); // /home/user/docs/file.txt
Path relativized = base.relativize(resolved); // docs/file.txt
}
}
Files 方法速查
| 方法 | 说明 |
|---|---|
readString(path) | 读取全部文本(JDK 11+) |
writeString(path, str) | 写入文本(JDK 11+) |
readAllLines(path) | 按行读取 |
readAllBytes(path) | 读取字节 |
copy(src, dst, opts) | 复制文件 |
move(src, dst, opts) | 移动文件 |
delete(path) / deleteIfExists(path) | 删除 |
createDirectories(path) | 递归创建目录 |
walk(path, depth) | 递归遍历 |
list(path) | 列出直接子项 |
find(path, depth, matcher) | 搜索文件 |
lines(path) | 惰性行流 |
exists(path) / isDirectory(path) | 属性检查 |
序列化
import java.io.*;
// 实现 Serializable 接口
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 版本号
private String name;
private transient String password; // transient 字段不序列化
private int age;
public User(String name, String password, int age) {
this.name = name;
this.password = password;
this.age = age;
}
// 序列化到文件
public static void serialize(User user, String path) throws IOException {
try (var oos = new ObjectOutputStream(new FileOutputStream(path))) {
oos.writeObject(user);
}
}
// 反序列化
public static User deserialize(String path) throws IOException, ClassNotFoundException {
try (var ois = new ObjectInputStream(new FileInputStream(path))) {
return (User) ois.readObject();
}
}
public static void main(String[] args) throws Exception {
User user = new User("张三", "secret123", 25);
serialize(user, "/tmp/user.ser");
User loaded = deserialize("/tmp/user.ser");
System.out.println("name: " + loaded.name);
System.out.println("age: " + loaded.age);
System.out.println("password: " + loaded.password); // null(transient)
}
}
⚠️ Java 原生序列化存在安全漏洞且性能较差。生产环境推荐使用 JSON(Jackson/Gson)或 Protocol Buffers。
⚠️ 注意事项
- 资源必须关闭 — 使用 try-with-resources 确保关闭。
- 编码问题 — 默认编码因系统而异,显式指定
StandardCharsets.UTF_8。 - 大文件不要一次性读入 — 使用流式读取,避免 OOM。
- NIO 文件锁是建议性的 — 不同操作系统行为不一致。
💡 技巧
一行代码读写文件(JDK 11+):
Files.writeString(Path.of("out.txt"), "内容"); String s = Files.readString(Path.of("out.txt"));临时文件:
Path tmp = Files.createTempFile("prefix", ".tmp"); tmp.toFile().deleteOnExit();类路径资源读取:
InputStream is = getClass().getResourceAsStream("/config.properties");
🏢 业务场景
- 文件上传/下载: 字节流 + BufferedInputStream 处理文件传输。
- 日志文件: BufferedWriter 追加写入日志。
- 配置文件: Files.readString 读取 YAML/JSON 配置。
- 数据导入导出: CSV 文件的读写处理。