强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Java 完全指南 / 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。

⚠️ 注意事项

  1. 资源必须关闭 — 使用 try-with-resources 确保关闭。
  2. 编码问题 — 默认编码因系统而异,显式指定 StandardCharsets.UTF_8
  3. 大文件不要一次性读入 — 使用流式读取,避免 OOM。
  4. NIO 文件锁是建议性的 — 不同操作系统行为不一致。

💡 技巧

  1. 一行代码读写文件(JDK 11+):

    Files.writeString(Path.of("out.txt"), "内容");
    String s = Files.readString(Path.of("out.txt"));
    
  2. 临时文件

    Path tmp = Files.createTempFile("prefix", ".tmp");
    tmp.toFile().deleteOnExit();
    
  3. 类路径资源读取

    InputStream is = getClass().getResourceAsStream("/config.properties");
    

🏢 业务场景

  • 文件上传/下载: 字节流 + BufferedInputStream 处理文件传输。
  • 日志文件: BufferedWriter 追加写入日志。
  • 配置文件: Files.readString 读取 YAML/JSON 配置。
  • 数据导入导出: CSV 文件的读写处理。

📖 扩展阅读