ElIauk

Java IO 与文件读写入门

字节流与字符流 Java IO 的类很多,先抓住一条主线:InputStream / OutputStream 处理字节,Reader / Writer 处理字符。图片、压缩包、Excel 这类二进制内容用字节流;txt、csv、json 这类文本用字符流,并且一定要指定编码。 字符流的底层其实还是

评论
Java IO 与文件读写入门

字节流与字符流

Java IO 的类很多,先抓住一条主线:InputStream / OutputStream 处理字节,Reader / Writer 处理字符。图片、压缩包、Excel 这类二进制内容用字节流;txt、csv、json 这类文本用字符流,并且一定要指定编码。

字符流的底层其实还是字节流,中间靠 InputStreamReader / OutputStreamWriter 做编码转换。JDK 7 之后多了 Files、Paths 这套 NIO 工具类,日常读写文件比以前短很多,新手可以直接从这里入手。

常见组合可以对照着记:

场景 字节流 字符流
文件 FileInputStream / FileOutputStream FileReader / FileWriter
缓冲 BufferedInputStream BufferedReader / BufferedWriter
内存 ByteArrayInputStream StringReader / StringWriter
编码转换 — InputStreamReader / OutputStreamWriter

直接读字节流时,建议再套一层 BufferedInputStream 或 BufferedOutputStream,否则每次 read 都可能触发一次系统调用。小文件感觉不出来,大文件能慢一个数量级。字符流同理,BufferedReader 的缓冲能显著减少 IO 次数。

Files 还提供了 copy、move、deleteIfExists、createTempFile 这些方法,比手写流简单。移动文件优先用 Files.move,它底层可能直接改目录项,不用真的把数据复制一遍。

用 Files 读写小文件

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;

public class FilesDemo {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("data", "note.txt");
        Files.createDirectories(path.getParent());
        Files.write(path, "第一行\n第二行".getBytes(StandardCharsets.UTF_8));

        List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
        lines.forEach(System.out::println);

        Files.write(path, "追加一行\n".getBytes(StandardCharsets.UTF_8),
                StandardOpenOption.APPEND);
        System.out.println(Files.size(path));
    }
}

Files.write 默认是覆盖,想追加要显式传 StandardOpenOption.APPEND。JDK 11 之后还有更简洁的 Files.readString / Files.writeString,写法和上面差不多。

大文件要逐行读

Files.readAllLines 会把整个文件读进内存,几十 MB 的日志还能扛,几百 MB 就可能直接 OOM。大文件用 BufferedReader 逐行处理,配合 try-with-resources 自动关闭:

try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        // 处理每一行,读完即丢
    }
}

readLine() 返回 null 表示读到文件末尾,注意判断的是 null,不是空字符串,空行是合法的内容。

编码和路径两个必知点

编码:FileReader、FileWriter、new String(bytes) 这些旧 API 不传编码时用的是平台默认编码,Windows 上通常是 GBK,Linux 上通常是 UTF-8。同一份代码本地正常、服务器乱码,多半就是这里出的问题。统一显式传 StandardCharsets.UTF_8。

路径:相对路径的基准是 JVM 启动目录(user.dir),不是 class 文件所在目录。IDE 里跑和打成 jar 之后跑,基准可能完全不同。生产环境建议用配置项给出绝对路径,不要依赖“当前目录”。拼接路径用 Paths.get("data", "note.txt"),不要手写 "data/" + name,省得跨平台踩分隔符的坑。

读 classpath 下的文件不能用 Paths.get,因为打成 jar 之后文件不在文件系统里,而是压缩包内的一个条目。用 getClass().getResourceAsStream("/config/app.properties") 拿到流,再交给 Properties 或 BufferedReader 读取,单元测试里读 src/test/resources 下的文件也是同一个道理。

临时文件用 Files.createTempFile("prefix", ".tmp"),系统会自动放到临时目录,程序退出后不保证还在,别拿它当持久化存储。需要目录遍历时用 Files.walk(path) 配合 try-with-resources,它返回一个 Stream<Path>,用完要关闭。

文件读写还要考虑并发:多个线程同时写同一个文件,内容会交错甚至损坏。需要并发写时按线程分文件,或者用单线程串行写;加锁只能解决同一进程内的问题,多实例部署时得靠外部存储。

常见坑

1. 忘记关闭流

Windows 上文件没关闭,删除时会提示“文件被另一个程序占用”;连接没关闭则会耗尽句柄。一律用 try-with-resources,不要手写 finally 里一堆 close()。

2. 编码不一致

写的时候 UTF-8,读的时候用默认编码,中文就变成问号或乱码。排查时先确认两端的编码,再看文件本身是不是 GBK 存的。

3. read 的返回值判断错

InputStream.read(buffer) 返回读到的字节数,读到末尾返回 -1。写成 while (in.read(buffer) > 0) 在长度为 0 时会提前退出,标准写法是 != -1。

4. 大文件一次读进内存

readAllBytes()、readAllLines() 都很方便,但只适合小文件。处理日志、导出数据时老老实实流式读,边读边写。

5. 换行符不一致

Windows 的换行是 \r\n,Linux 是 \n。用 readLine() 读会去掉换行,但如果自己按字节读,行尾可能带 \r,导致 equals("ok") 永远为 false。

排查步骤

  1. NoSuchFileException:打印 path.toAbsolutePath(),确认程序实际找的是哪个目录;
  2. AccessDeniedException:检查文件权限和是否被其他进程占用;
  3. 中文乱码:确认写入、读取两端的编码,统一成 UTF-8;
  4. MalformedInputException:文件不是 UTF-8 编码,用 file -i(Linux)或记事本另存确认;
  5. 内存持续上涨:检查有没有把大文件整体读进集合里。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者