CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 语言基础/课时/文件 IO 入门
RawBlame
课时目录14 节

Java 语言基础

  1. 01Java 与 JVM 是什么
  2. 02基本语法与 main
  3. 03基本类型与引用类型
  4. 04条件与循环
  5. 05方法与参数
  6. 06数组
  7. 07类与封装
  8. 08继承接口与多态
  9. 09集合框架入门
  10. 10字符串
  11. 11异常处理
  12. 12文件 IO 入门
  13. 13Maven 与项目结构
  14. 14综合练习与 dsa-java 分工

文件 IO 入门

Path、Files、try-with-resources。

30 分钟

文件 IO 入门

这一课你将能做到

  1. 用 JDK 11+ 的 Files.readString/writeString 完成文本读写,用 BufferedReader 逐行处理大文件
  2. 理解字节流与字符流的区别,知道什么时候用哪一个、编码为什么必须显式指定 UTF-8
  3. 用 try-with-resources 安全操作文件——把上一课的异常知识落到资源管理实战

为什么要学

后端离不开文件 IO:本站场景里,日志落盘、配置文件读取(application.yml)、用户上传头像存储、Excel 导入导出、生成导出报表——底层全是文件操作。

Java 的 IO 类库历史悠久(1996 年的 java.io、2002 年的 NIO、2007 年的 NIO.2),新手最容易被 File、FileInputStream、Reader、Path、Files 十几个类绕晕。本课直接采用现代写法:JDK 7+ 的 Path/Files + JDK 11+ 的便捷读写方法,覆盖日常 90% 场景,老 API 只需认识(以后读别人代码会碰到)。

先建立图像

数据的外壳          数据的内容
Path(路径)  ──▶  Files(操作)  ──▶  内容
"demo.txt"        readString /        "你好\nJava"
                   writeString /
                   copy / walk

字节流 vs 字符流:
  字节流 InputStream/OutputStream  → 处理二进制(图片、zip、视频),按 8 位字节
  字符流 Reader/Writer             → 处理文本,按「字符」读,自动解码
                                     字符流必须知道编码(UTF-8/GBK),否则中文乱码

跟着做

步骤 1:三行读写一个文件

新建 QuickIo.java:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class QuickIo {
    public static void main(String[] args) throws IOException {
        Path file = Path.of("demo.txt");          // Path.of 是 JDK 11+ 的写法

        // 写入:整个字符串一次写完(默认 UTF-8;不存在则创建,存在则覆盖)
        Files.writeString(file, "你好, CodeRoadMap\n第二行: Spring Boot + MySQL + Redis");
        System.out.println("文件已生成: " + file.toAbsolutePath());

        // 读取:整个文件一次读进内存
        String content = Files.readString(file);
        System.out.println("---- 文件内容 ----");
        System.out.println(content);
    }
}

适用边界:readString 一次载入全量内容,适合配置、小文件(几 MB 内)。几百 MB 的日志用它会把堆撑爆——逐行读见步骤 3。

步骤 2:常用 Files 操作集

import java.io.IOException;
import java.nio.file.*;

public class FilesOps {
    public static void main(String[] args) throws IOException {
        Path dir = Path.of("data");
        Path src = dir.resolve("a.txt");        // resolve:拼子路径(跨平台分隔符)
        Path dst = Path.of("data", "b.txt");    // 多段拼接

        Files.createDirectories(dir);           // 建多级目录(已存在不报错)
        Files.writeString(src, "hello io");

        System.out.println(Files.exists(src));  // true
        System.out.println(Files.isRegularFile(src)); // true
        System.out.println(Files.size(src));    // 8(字节数)

        // 复制 / 移动 / 删除
        Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
        Files.move(src, Path.of("data", "renamed.txt"),
                StandardCopyOption.REPLACE_EXISTING);
        Files.deleteIfExists(dst);              // 存在才删,避免 NoSuchFileException

        // 遍历目录(一层)
        try (var entries = Files.list(dir)) {
            entries.forEach(e -> System.out.println("条目: " + e.getFileName()));
        }
        // 深度递归遍历用 Files.walk(dir)
    }
}

注意 Files.list 返回的是需要关闭的流,必须 try-with-resources 包住——这是新手最容易漏的资源泄漏点。

步骤 3:逐行读大文件

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.*;

public class LineReaderDemo {
    public static void main(String[] args) throws IOException {
        Path log = Path.of("access.log");
        // 造一份模拟访问日志
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= 3; i++) {
            sb.append("2026-10-06 10:0").append(i)
              .append(" GET /api/user/").append(1000 + i)
              .append(" 200\n");
        }
        Files.writeString(log, sb.toString());

        // 方式 1:BufferedReader 逐行(内存恒定占用,适合任意大文件)
        try (BufferedReader reader = Files.newBufferedReader(log)) {
            String line;
            while ((line = reader.readLine()) != null) {   // 读到末尾返回 null
                if (line.contains("user/1001")) {
                    System.out.println("命中: " + line);
                }
            }
        }

        // 方式 2:Files.lines 流式处理(函数式风格,同样要关闭)
        try (var lines = Files.lines(log)) {
            long count = lines.filter(l -> l.contains(" 200")).count();
            System.out.println("200 响应条数: " + count);
        }
    }
}

步骤 4:字符流与编码

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;

public class EncodingDemo {
    public static void main(String[] args) throws IOException {
        Path f = Path.of("utf8.txt");
        String text = "中文内容 Chinese";

        // 写:显式指定 UTF-8(writeString 默认就是 UTF-8)
        Files.writeString(f, text, StandardCharsets.UTF_8);

        // 读:编码不一致 → 乱码或 MalformedInputException
        String ok = Files.readString(f, StandardCharsets.UTF_8);
        System.out.println("UTF-8 读回: " + ok);

        try {
            Files.readString(f, StandardCharsets.US_ASCII);   // 用错编码
        } catch (Exception e) {
            System.out.println("编码不符: " + e.getClass().getSimpleName());
        }

        // 字节流侧写:文本 → 字节 → 还原(理解「字符 = 编码后的字节」)
        byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
        System.out.println("UTF-8 字节数: " + bytes.length);   // 汉字 3 字节/个
        String back = new String(bytes, StandardCharsets.UTF_8);
        System.out.println("还原: " + back);
    }
}

原则:编码永远显式写 UTF-8,不依赖平台默认值。Windows 老版本默认 GBK,Linux 默认 UTF-8——不指定就是跨平台乱码地雷。

步骤 5:字节流处理二进制

import java.io.*;
import java.nio.file.*;

public class BinaryDemo {
    public static void main(String[] args) throws IOException {
        Path src = Path.of("demo-src.bin");
        Path dst = Path.of("demo-dst.bin");

        // 造 1MB 随机二进制数据(模拟图片/zip)
        byte[] data = new byte[1024 * 1024];
        new java.util.Random(42).nextBytes(data);
        Files.write(src, data);

        // 字节流复制:InputStream / OutputStream,8KB 缓冲区
        try (InputStream in = Files.newInputStream(src);
             OutputStream out = Files.newOutputStream(dst)) {
            byte[] buffer = new byte[8192];
            int n;
            while ((n = in.read(buffer)) != -1) {   // read 返回 -1 表示读完
                out.write(buffer, 0, n);
            }
        }
        System.out.println("复制完成,大小一致: " + (Files.size(src) == Files.size(dst)));

        // 小文件也可以一步复制
        Files.copy(src, Path.of("demo-dst2.bin"), StandardCopyOption.REPLACE_EXISTING);
    }
}

步骤 6:后端场景串烧——简易 KV 存储

import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;

public class FileKvStore {
    private final Path file = Path.of("kv-store.txt");   // 模拟持久化(Redis 落盘的极简版)

    public void save(Map<String, String> data) throws IOException {
        StringBuilder sb = new StringBuilder();
        data.forEach((k, v) -> sb.append(k).append("=").append(v).append("\n"));
        Files.writeString(file, sb.toString());          // 全量覆盖写
    }

    public Map<String, String> load() throws IOException {
        Map<String, String> data = new HashMap<>();
        if (Files.notExists(file)) return data;
        try (var lines = Files.lines(file)) {
            lines.forEach(l -> {
                int i = l.indexOf('=');
                if (i > 0) data.put(l.substring(0, i), l.substring(i + 1));
            });
        }
        return data;
    }

    public static void main(String[] args) throws IOException {
        FileKvStore store = new FileKvStore();
        Map<String, String> m = new HashMap<>();
        m.put("user:1001", "tom");
        m.put("user:1002", "jerry");
        store.save(m);

        System.out.println("重启后读回: " + store.load());
        // 生产里这就是「内存缓存 + 定期落盘」的思路,Redis 的 RDB 快照同理
    }
}

讲清楚要点

  1. Path/Files 是现代标准:Path.of("a/b.txt") 创建路径,Files.xxx 做所有操作;java.io.File 是遗留 API,只在旧代码里见到(file.toPath() 可转现代路径)。
  2. 读写选型:小文件 readString/writeString;大文件/逐行 BufferedReader/Files.lines;二进制 InputStream/OutputStream + 缓冲区;已知字节全量 Files.readAllBytes。
  3. 编码必须显式:所有读写字符的方法都有带 Charset 参数的重载,统一传 StandardCharsets.UTF_8。
  4. 资源必须关闭:Files.list、Files.walk、Files.lines、各种流都实现 AutoCloseable,全部套进 try-with-resources。文件句柄泄漏在长跑的服务里会拖垮系统。
  5. 异常模型:IO 异常都是受检的 IOException(呼应上一课),要么 catch 要么 throws——因为「磁盘、权限、文件被删」都是程序外因素,编译器逼你处理。
  6. 覆盖 vs 追加:writeString 默认覆盖;追加用 Files.writeString(file, text, StandardOpenOption.CREATE, StandardOpenOption.APPEND)。日志场景就是追加写。

你可能会懵的地方

  • File 和 Path 什么关系? File 是 JDK 1.0 的遗留类,方法命名混乱(mkdir 失败不报原因)、不支持符号链接、错误只能拿到 false。NIO.2 的 Path 修复了这一切,两者可通过 file.toPath() / path.toFile() 互转。新代码一律 Path + Files。

  • 为什么 Files.lines 要 try-with-resources,Files.readString 不用? readString 返回 String(纯数据,无资源);lines 返回懒加载的 Stream,底层持有打开的文件句柄,不关闭就泄漏。判断标准:返回的是「数据」还是「带句柄的流」。

  • readLine 返回 null 和空串的区别? null = 文件结束(EOF);空串 = 该行没有内容(连续换行符)。所以循环条件写 != null,不要写成 !line.isEmpty()。

  • 为什么二进制文件不能用 readString 读? 图片/zip 的字节序列不一定能解码成合法字符,readString 会抛 MalformedInputException 或产出损坏数据。文本走字符流,二进制走字节流,泾渭分明。

  • 写入的数据立刻可见吗?会丢吗? writeString 默认写入后即对同进程可见;但 OS 有页缓存,机器断电可能丢最后几 KB。要强持久化用 FileChannel.force(true)——生产日志/存储引擎的 fsync 就是这层。学习阶段不必展开,知道「写进 Java ≠ 落盘」即可。

  • 大文件逐行读会不会 OOM? 不会。BufferedReader 按行读,内存里同时只有一行;Files.lines 也是流式惰性求值。会 OOM 的是 readString/readAllBytes 这类全量载入。

错法对照

错法问题正解
Files.readString(Path.of("big.log")) 读 GB 级文件OOMBufferedReader / Files.lines 逐行
流没关闭(手写 new BufferedReader 不 close)句柄泄漏try-with-resources
读文件不指定编码Windows 中文乱码统一 StandardCharsets.UTF_8
Files.delete(path) 文件不存在抛 NoSuchFileExceptionFiles.deleteIfExists(path)
用 File 的 mkdir() 判断成功与否失败原因被吞成 falseFiles.createDirectories(path),失败抛异常带原因
字节流读文本后 new String(bytes) 不给编码平台相关乱码new String(bytes, StandardCharsets.UTF_8)
Windows 路径硬编码 C:\dir\a.txt 单反斜杠\a 被当转义符Path.of("C:", "dir", "a.txt") 或正斜杠 C:/dir/a.txt

卡点急救

  1. NoSuchFileException(读):路径拼错或文件没创建。先 Files.exists(path) 检查,打印 path.toAbsolutePath() 核对实际位置——相对路径基于「启动命令的当前目录」,不是源码目录。
  2. AccessDeniedException:目标目录无写权限(Linux 常见)、或文件正被其他进程占用(Windows 的 Excel/编辑器锁文件)。换目录或关占用程序。
  3. 中文乱码:三方对齐——写文件编码、读文件编码、终端显示编码统一 UTF-8;Windows 老终端先 chcp 65001。
  4. DirectoryNotEmptyException:delete 只删空目录,递归删除要 Files.walk 先删文件再删目录(谨慎,删前打印确认)。
  5. Files.lines 编译报「未处理的异常」:它抛受检 IOException,try/catch 或方法 throws(上一课的知识现在闭环了)。

面试常问

  • 字节流和字符流的区别? 字节流按 8 位字节处理原始二进制(InputStream/OutputStream);字符流按字符处理文本,内部完成字节↔字符的编解码(Reader/Writer),需要指定编码。文本用字符流,图片/音视频用字节流。

  • Java IO 有哪些层次? 装饰器模式:节点流(FileInputStream 直接连数据源)+ 处理流(BufferedInputStream 加缓冲、DataInputStream 读基本类型)。new BufferedReader(new FileReader(f)) 就是典型装饰。

  • BIO、NIO、AIO 的区别?(后端进阶题) BIO 同步阻塞(一线程一连接,Tomcat 传统模式);NIO 同步非阻塞(多路复用 Selector,Netty 底座);AIO 异步非阻塞(回调,Linux 支持有限,用得少)。本课的 Files/NIO.2 是文件系统 API,与网络 NIO 同属 java.nio 包但不是一回事。

  • 如何高效读写大文件? 逐行流式处理(BufferedReader/Files.lines)而非全量载入;复制用带缓冲区的循环 read/write 或 Files.copy;超大规模考虑 FileChannel + MappedByteBuffer(内存映射)。

  • try-with-resources 的原理? 编译器在 try 结束(正常或异常路径)自动生成 close() 调用,并把 close 的异常处理为 suppressed exception 挂到主异常上。要求资源实现 AutoCloseable。

自测三问

  1. 读一个 2GB 的日志文件统计 ERROR 行数,写出代码骨架(选哪个 API、怎么关资源)。
  2. Files.readString 返回的 String 需要关闭吗?Files.lines 返回的 Stream 呢?为什么?
  3. 字节流复制文件时 in.read(buffer) 返回 0、-1、n>0 分别代表什么?循环条件怎么写?

小结一句话

现代 Java IO = Path 定位 + Files 操作 + try-with-resources 保安全;小文件 readString、大文件逐行流、二进制字节流,编码永远显式 UTF-8——日志、配置、上传存储全靠这套路子。

延伸

  • Oracle:基本 IO
  • 下一课:Maven 与项目结构——告别 javac 手工编译,走进本站后端真实的多模块工程。
加载本节练习…
← 上一课异常处理下一课 →Maven 与项目结构