在Java标准库中,java.nio.file.Files类提供了一组静态方法,用于以简洁且统一的方式处理文件与目录。它建立在NIO的Path接口之上,屏蔽了底层文件系统的差异,让开发者不必再纠结于FileInputStream的嵌套包装。无论是读取配置、写入日志还是批量迁移目录,Files都能用一两行代码解决。

一、Path的构造与基础判断
使用Files之前必须先拿到Path对象。Paths.get是最常用的入口,它接受字符串或URI,并按照当前系统规则拼接路径。在Windows与Linux下分隔符不同,但Path会帮我们归一化,避免硬编码反斜杠带来的移植问题。
拿到Path后,可以用Files.exists、Files.isRegularFile等方法做前置校验。与旧的File.exists不同,这些方法在遇到符号链接时可以通过LinkOption参数控制是否跟随,从而在安全扫描类程序中减少误判。
import java.nio.file.*;
import java.nio.file.attribute.*;
public class PathDemo {
public static void main(String[] args) throws Exception {
Path path = Paths.get("/data", "app.log");
// 不跟随符号链接判断是否存在
boolean exists = Files.exists(path, LinkOption.NOFOLLOW_LINKS);
System.out.println("文件存在:" + exists);
// 判断是否为普通文件
boolean isFile = Files.isRegularFile(path);
System.out.println("是普通文件:" + isFile);
}
}
二、文本文件的读取方式对比
Files.readAllLines适合小到中等体积的文本文件,它会一次性读入内存并返回List<String>,编码默认UTF-8。这种方法代码最短,但若是几百兆的日志就会撑爆堆空间,因此只建议用在启动配置等可控场景。
对于大文件或需要逐行处理的任务,Files.newBufferedReader返回的BufferedReader可以配合try-with-resources自动关闭,既保留流式处理优势,又不必写finally块。下面示例展示两种写法,后者明显更适合生产环境。
import java.nio.file.*;
import java.io.*;
import java.util.List;
public class ReadDemo {
public static void main(String[] args) throws IOException {
Path p = Paths.get("config.txt");
// 方式一:小文件整读
List<String> lines = Files.readAllLines(p);
lines.forEach(System.out::println);
// 方式二:大文件流式读
try (BufferedReader br = Files.newBufferedReader(p)) {
String line;
while ((line = br.readLine()) != null) {
if (line.contains("error")) {
System.out.println("发现错误行:" + line);
}
}
}
}
}
三、写入与复制的高效实践
写入文本时Files.write可以接收Iterable<? extends CharSequence>,并指定StandardOpenOption控制覆盖或追加。若需写入二进制,则使用Files.copy(InputStream, Path)直接从流落地,省去手动缓冲数组拷贝。
目录间迁移文件推荐Files.copy(Path, Path, CopyOption...),传入StandardCopyOption.REPLACE_EXISTING可覆盖目标,ATOMIC_MOVE则保证移动操作的原子性,避免程序崩溃导致半截文件。下面的例子演示了带覆盖选项的复制。
import java.nio.file.*;
import java.nio.file.attribute.*;
import static java.nio.file.StandardCopyOption.*;
public class CopyDemo {
public static void main(String[] args) throws IOException {
Path src = Paths.get("a.txt");
Path dst = Paths.get("backup/a.txt");
// 确保目标目录存在
Files.createDirectories(dst.getParent());
// 覆盖式复制
Files.copy(src, dst, REPLACE_EXISTING);
System.out.println("复制完成");
}
}
四、目录遍历与常见坑
Files.walk返回Stream<Path>,能以深度优先方式遍历目录树,配合filter做条件筛选非常方便。但Stream不会自动关闭底层目录资源,必须用try-with-resources包裹,否则在遍历海量子目录时会出现系统句柄耗尽。
另一个易错点是在walk中做删除操作:若先删父目录会导致后续遍历抛NoSuchFileException。正确做法是使用Files.walkFileTree并重写visitFile与postVisitDirectory,在离开目录后再删除自身,保证顺序安全。
import java.nio.file.*;
import java.io.IOException;
public class WalkDemo {
public static void main(String[] args) throws IOException {
Path root = Paths.get("logs");
// 必须放在try里以关闭目录流
try (var stream = Files.walk(root)) {
stream.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".tmp"))
.forEach(p -> {
try { Files.delete(p); } catch (IOException e) { e.printStackTrace(); }
});
}
}
}
五、异常处理与权限排查
Files抛出的都是IOException子类,例如NoSuchFileException、AccessDeniedException,比旧File返回false更利于定位。在运维脚本中应当捕获具体子类,给出清晰提示而不是笼统打印堆栈。
当遇到权限问题时,可先用Files.getPosixFilePermissions读取当前模式位,结合用户身份判断缺失哪类权限。在容器环境里常因挂载目录的uid映射导致写入失败,这时用Files.isWritable做快速探测能缩短排障时间。
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;
import java.util.Set;
public class PermDemo {
public static void main(String[] args) {
Path p = Paths.get("/var/data");
try {
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(p);
System.out.println("权限集合:" + perms);
} catch (AccessDeniedException e) {
System.out.println("无权限访问,请检查挂载或用户组");
} catch (IOException e) {
System.out.println("其他IO错误:" + e.getMessage());
}
}
}