现代文件读写救急锦囊: Files.readString / writeString
Java 11+ 读写小文件一把梭:不用 FileReader/FileWriter 那套老写法。
一句话定调
读写小配置文件,用 Files.readString() / Files.writeString() 一把梭,默认 UTF-8,自动关闭资源。
为什么老写法该退休
new FileReader("a.txt") 这套 IO 流写法有三宗罪:要手写 try-finally 关闭、默认平台编码(Windows 上 GBK,Linux 上 UTF-8,同一个文件跨平台读出两种结果)、五六行样板才能读出一个字符串。java.nio.file.Files 的静态方法一次解决:内部自己开关资源、readString 默认 UTF-8、一行读全文件。记住一个心智模型——老 API 是”自来水管”(一边流一边用),新 API 是”矿泉水瓶”(整瓶搬运):小文件直接拿瓶,大文件(百 MB 以上)老老实实用流。
极简代码(看懂这 20 行就够了)
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class FileQuickstart {
public static void main(String[] args) throws IOException {
Path path = Path.of("note.txt");
// 写入:不存在则创建,已存在则覆盖
Files.writeString(path, "第一行内容\n第二行内容");
// 读取:一次性读回整个文件
String text = Files.readString(path);
System.out.println(text);
// 预期输出:
// 第一行内容
// 第二行内容
// 追加一行:需要显式声明 APPEND
Files.writeString(path, "追加行\n", StandardOpenOption.APPEND);
// 按行读:小文件直接用 readAllLines
for (String line : Files.readAllLines(path)) {
System.out.println("行: " + line);
}
// 预期输出:
// 行: 第一行内容
// 行: 第二行内容
// 行: 追加行
}
}
注意:方法签名带 throws IOException,要么在方法上声明,要么用 try-catch(结合 017 try-with-resources 与 016 异常处理)。
完整示例:一行不用流读完 + 常用文件操作全家福
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FilesKitchen {
public static void main(String[] args) throws IOException {
Path src = Path.of("demo.txt");
// 按行处理大一点的文件(惰性读取,不一次性装进内存)
Files.writeString(src, "alpha\nbeta\ngamma");
long count = Files.lines(src).filter(l -> !l.isEmpty()).count();
System.out.println("非空行数: " + count); // 预期输出:非空行数: 3
// 判断 / 元信息
System.out.println(Files.exists(src)); // true
System.out.println(Files.size(src)); // 16(UTF-8 下每个英文字母 1 字节,3 个换行符)
// 复制 / 移动 / 删除
Files.copy(src, Path.of("demo-copy.txt"));
Files.move(Path.of("demo-copy.txt"), Path.of("demo-renamed.txt"));
Files.delete(Path.of("demo-renamed.txt"));
// 临时文件:自动起名,用完即删
Path tmp = Files.createTempFile("log-", ".txt");
System.out.println("临时文件: " + tmp.getFileName()); // 类似 log-8371492.txt
Files.delete(tmp);
Files.delete(src);
}
}
Files.lines 返回 Stream<String>,记得用 try-with-resources 包住(流持有文件句柄),或像上例一样当场消费干净。
指定编码:跨平台别赌默认值
readString/writeString 默认 UTF-8(JDK 18 起整个 JVM 默认编码也是 UTF-8)。读写历史遗留的 GBK 文件时显式指定,避免乱码:
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
// 读 GBK 编码的老文件
String gbkText = java.nio.file.Files.readString(
java.nio.file.Path.of("old.txt"), Charset.forName("GBK"));
// 写 UTF-8(显式写出意图)
java.nio.file.Files.writeString(
java.nio.file.Path.of("new.txt"), "内容", StandardCharsets.UTF_8);
如果报这个错,看这里
报错:java.nio.file.NoSuchFileException: a.txt
原因:路径不存在或相对路径的工作目录和你以为的不一样。
对策:先用 Files.exists(path) 判断;相对路径是相对程序启动目录,不确定时用绝对路径或 Paths.get("src", "main", "resources", "a.txt")。
报错:FileSystemException: 另一个程序正在使用此文件,进程无法访问(Windows)
原因:文件被编辑器/Excel 占用。
对策:关闭占用程序;写入用 StandardOpenOption.TRUNCATE_EXISTING 配合重试,或先写临时文件再 Files.move 原子替换。
报错:java.nio.file.FileAlreadyExistsException
原因:Files.copy/move/createFile 遇到同名目标,默认不覆盖。
对策:加 StandardOpenOption.REPLACE_EXISTING(copy/move 均支持),或先判断再决定覆盖策略。
现象:读回来全是乱码
原因:文件实际编码与解码编码不一致(最常见是 GBK 文件被按 UTF-8 解)。
对策:显式传 Charset 参数;无法确定编码时,先看文件头几个字节(BOM)或与文件生产方确认,Java 无法”自动识别编码”。
现象:大文件用 readString 后内存暴涨或 OOM
原因:readString/readAllLines 把整个文件装进内存。
对策:超过几十 MB 改用 Files.lines(按行流式)或 028 的 BufferedReader 流式处理。
进阶配方:目录遍历与流式统计
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DirWalkDemo {
public static void main(String[] args) throws IOException {
Path dir = Path.of("src");
// 递归列出目录下所有 .java 文件(惰性遍历,目录大也稳)
try (Stream<Path> entries = Files.walk(dir)) {
Map<String, Long> byExt = entries
.filter(Files::isRegularFile)
.collect(Collectors.groupingBy(
p -> extension(p.getFileName().toString()),
Collectors.counting()));
System.out.println(byExt);
// 预期输出(按实际目录内容):{java=12, md=3}
}
}
static String extension(String name) {
int dot = name.lastIndexOf('.');
return dot < 0 ? "无扩展名" : name.substring(dot + 1);
}
}
Files.walk 返回的流持有目录句柄,务必 try-with-resources;只要文件名列表不需要递归,用 Files.list(仅当前目录)即可。这类”walk + 过滤 + 收集”的写法比手写 File 递归少一半代码,也少一半出错机会。
记住
初学者记住三点:
- 小文件:
Files.writeString写、Files.readString读、Files.readAllLines按行读。- 默认 UTF-8;非 UTF-8 文件显式传
Charset,别赌平台默认编码。- 方法都抛受检异常
IOException,先throws跑通再细化处理。
进阶者还需注意:
Files.lines是惰性流且持有文件句柄,务必 try-with-resources 或当场消费。- 原子写模式:写临时文件 +
Files.move(..., ATOMIC_MOVE, REPLACE_EXISTING),避免读方读到写了一半的内容。 - 目录树遍历用
Files.walk/Files.walkFileTree;删除整个目录要自己递归,Files.delete不删非空目录。