Java IO——IO工具

本文最后更新于:9 天前

Path 类

概述

在 Java NIO(java.nio.file)中,Path 类是 Java 7 引入的新文件路径处理 API,属于 java.nio.file 包的一部分。相比于旧的 java.io.FilePath 具有更强大的功能,包括:

  • 支持跨平台路径处理(自动适配 Windows 和 Unix/Linux)
  • 支持相对路径与绝对路径
  • 提供丰富的路径操作方法
  • 支持符号链接(Symbolic Links)
  • Files 类搭配使用,实现强大的文件操作

Path 不是文件或目录本身,而是文件路径的抽象表示,它可以表示一个文件或目录的路径,既可以是相对路径,也可以是绝对路径。

构造方法

Path 对象通过 java.nio.file.Paths 类的静态方法构造。

  • Path get(String first, String... more):通过字符串创建 Path 对象。
  • Path get(URI uri):通过 URI 对象创建 Path 对象。

常用方法

  • FileSystem getFileSystem():返回创建此对象的文件系统。
  • boolean isAbsolute():判断是否为绝对路径。
  • Path getRoot():获取根路径。
  • Path getFileName():获取文件名。
  • Path getParent():获取父路径。
  • int getNameCount():获取路径层级数量。
  • Path getName(int index):获取层级(目录或文件)的名称。
  • Path subpath(int beginIndex, int endIndex):返回一个相对路径,该路径是此路径的名称元素的子序列。
  • Path toAbsolutePath():将路径转换为绝对路径。
  • boolean startsWith(Path other):判断此路径是否从给定路径开始。
  • boolean startsWith(String other):判断此路径是否从给定路径开始。
  • boolean endsWith(Path other):判断此路径是否从给定路径结束。
  • boolean endsWith(String other):判断此路径是否从给定路径结束。
  • Path resolve(Path other):拼接两个 Path。
  • Path relativize(Path other):构造此路径和给定路径之间的相对路径。
  • URI toUri():返回此路径的 URI。
  • File toFile():返回此路径标识的 File 对象。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class PathDemo {
public static void main(String[] args) throws IOException, URISyntaxException {
// 创建 Path 对象1
Path path1 = Paths.get("nio-demo.txt");
// 创建 Path 对象2
Path path2 = Paths.get(new URI("file:///nio-demo.txt"));

// 返回创建此对象的文件系统
System.out.println("文件系统:" + path1.getFileSystem());

// 转换为绝对路径
path1 = path1.toAbsolutePath();
System.out.println("转换为绝对路径:" + path1);

// 是否为绝对路径
System.out.println("是否为绝对路径:" + path1.isAbsolute());

// 获取根路径
System.out.println("根路径:" + path1.getRoot());

// 获取文件路径层级信息
System.out.println("文件路径层级数量:" + path1.getNameCount());
for (int i = 0; i < path1.getNameCount(); i++) {
System.out.println("层级 " + i + ":" + path1.getName(i));
}

// 返回 1-2 之间的子路径
System.out.println("子路径:" + path1.subpath(1, 2));

// 是否以指定路径开始或结束
System.out.println("是否以指定路径开始:" + path1.startsWith(Paths.get("C:/")));
System.out.println("是否以指定路径结束:" + path1.endsWith("nio-demo.txt"));

// 拼接路径
System.out.println("拼接路径:" + path1.resolve("nio-demo2.txt"));

// 与 C:/ 的相对路径
System.out.println("与 C:/ 的相对路径:" + path1.relativize(Paths.get("C:/")));

// 返回路径 URI
System.out.println("路径 URI:" + path1.toUri());

// 返回 File 对象
System.out.println("File 对象:" + path1.toFile());
}
}

Files 类

概述

java.nio.file.Files 是 Java 7 引入的文件和目录操作工具类,提供了大量静态方法来简化文件创建、删除、复制、移动、读取、写入、属性管理等 操作。相比 java.io.FileFiles 类功能更丰富,并且可以与 Path 类无缝结合,适用于现代 Java 项目。

常用方法

  • Path createFile(Path path, FileAttribute<?>... attrs):创建文件。
  • Path createDirectory(Path dir, FileAttribute<?>... attrs):创建单层目录。
  • Path createDirectories(Path dir, FileAttribute<?>... attrs):创建多层目录。
  • Path createTempFile(String prefix, String suffix, FileAttribute<?>... attrs):创建临时文件。
  • Path createTempDirectory(String prefix, FileAttribute<?>... attrs):创建临时目录。
  • void delete(Path path):删除文件。
  • boolean deleteIfExists(Path path):安全地删除。
  • Path copy(Path source, Path target, CopyOption... options):将文件复制到目标文件。
  • Path move(Path source, Path target, CopyOption... options):将文件移动或重命名为目标文件。
  • Path write(Path path, byte[] bytes, OpenOption... options):将字节写入文件(通过参数控制追加、新建等)。
  • BufferedWriter newBufferedWriter(Path path, OpenOption... options):打开或创建用于写入的文件,返回BufferedWriter以高效的方式将文本写入文件。
  • List<String> readAllLines(Path path):读取文件中的所有行(UTF-8 字符集)。
  • BufferedReader newBufferedReader(Path path):打开文件进行读取,返回BufferedReader以高效的方式从文件中读取文本。
  • boolean exists(Path path, LinkOption... options):判断文件是否存在。
  • boolean notExists(Path path, LinkOption... options):判断文件是否不存在。
  • boolean isRegularFile(Path path, LinkOption... options):判断是否为文件。
  • boolean isDirectory(Path path, LinkOption... options):判断是否为目录。
  • boolean isHidden(Path path):判断文件是否被隐藏。
  • boolean isReadable(Path path):判断文件是否可读。
  • Stream<Path> list(Path dir):遍历目录下的文件。
  • Stream<Path> walk(Path start, FileVisitOption... options):递归遍历目录下的文件。
  • InputStream newInputStream(Path path, OpenOption... options):创建文件的输入流。
  • OutputStream newOutputStream(Path path, OpenOption... options):文件文件的输出流。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
@Slf4j
public class FilesDemo {
public static void main(String[] args) throws IOException {
Path path = Paths.get("files-nio-demo.txt");
// 创建文件
Files.createFile(path);

// 创建单层目录
Files.createDirectory(Paths.get("files-nio-demo-dir"));

// 创建多层目录
Files.createDirectories(Paths.get("files-nio-demo-dir1/dir2/dir3"));

// 创建临时文件
Path tempFile = Files.createTempFile("temp-files", ".txt");

// 创建临时目录
Path tempDir = Files.createTempDirectory("temp-dir");

// 删除文件
Files.delete(path);

// 删除目录(安全模式,不抛出异常)
Files.deleteIfExists(Paths.get("files-nio-demo-dir"));

// 复制文件(覆盖已存在的文件)
Files.copy(Paths.get("source.txt"), Paths.get("files-copy.txt"), StandardCopyOption.REPLACE_EXISTING);

// 移动文件(覆盖已存在的文件)
Files.move(Paths.get("source.txt"), Paths.get("files-move.txt"), StandardCopyOption.REPLACE_EXISTING);

// 写入文本
Files.write(path, "Hello, Files!".getBytes());

// 追加文本
Files.write(path, "Hello, Files!".getBytes(), StandardOpenOption.APPEND);

// 使用 BufferedWriter 写入文本
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writer.write("Hello, Files!");
} catch (IOException e) {
log.error("写入文件失败", e);
}

// 读取所有行
List<String> strings = Files.readAllLines(Paths.get("files-nio-demo.txt"));

// 使用 BufferedReader 读取文件
List<String> lines = new ArrayList<>();
try (BufferedReader reader = Files.newBufferedReader(Paths.get("files-nio-demo.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
lines.add(line);
}
} catch (IOException e) {
log.error("读取文件失败", e);
}

// 判断文件是否存在
System.out.println(Files.exists(path));

// 判断文件是否不存在
System.out.println(Files.notExists(path));

// 判断是否为文件
System.out.println(Files.isRegularFile(path));

// 判断是否为目录
System.out.println(Files.isDirectory(Paths.get("files-nio-demo-dir1")));

// 判断是否为隐藏文件
System.out.println(Files.isHidden(path));

// 判断是否可读
System.out.println(Files.isReadable(path));

// 遍历目录下的文件
Files.list(Paths.get("files-nio-demo-dir1")).forEach(System.out::println);

// 递归遍历目录下的文件
Files.walk(Paths.get("files-nio-demo-dir1")).forEach(System.out::println);

// 递归遍历目录下的文件(不包含目录)
Files.walk(Paths.get("files-nio-demo-dir1"), FileVisitOption.FOLLOW_LINKS).forEach(System.out::println);

// 创建 InputStream
try (InputStream inputStream = Files.newInputStream(path)) {
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
System.out.println(new String(bytes));
} catch (IOException e) {
log.error("读取文件失败", e);
}

// 创建 OutputStream
try (OutputStream outputStream = Files.newOutputStream(path)) {
outputStream.write("Hello, Files!".getBytes());
} catch (IOException e) {
log.error("写入文件失败", e);
}

}
}

FileVisitor

介绍

java.nio.file.FileVisitor<T> 是 Java NIO 提供的文件遍历接口,用于递归遍历目录及其子目录,适用于文件搜索、批量删除、复制、统计等操作。

优势

  • 支持递归遍历,可以深入子目录,无需手动管理递归调用。

  • 提供四种回调方法,对每个文件和目录执行特定操作。

  • 相比 Files.list()Files.walk()FileVisitor 可自定义行为,如:

    • 统计文件大小

    • 删除非空目录

    • 过滤特定类型文件

    • 处理符号链接

对比 Files.walk()

方式 优点 缺点
Files.walk() 代码简洁,适用于简单遍历 控制力较弱,无法细粒度定制操作
FileVisitor 可精准控制遍历过程,如提前终止 代码较 walk() 复杂

四个回调方法

FileVisitor<T> 通过回调机制处理文件遍历,每当访问一个文件或目录时,会触发以下方法:

方法 触发时机 用途
preVisitDirectory(T dir, BasicFileAttributes attrs) 进入目录前 可用于记录访问路径、统计目录数量等
visitFile(T file, BasicFileAttributes attrs) 访问文件时 适用于文件分析、统计大小、复制等
visitFileFailed(T file, IOException exc) 访问文件失败 处理无权限、文件损坏等异常
postVisitDirectory(T dir, IOException exc) 退出目录后 可用于删除空目录等

方法返回值

  • FileVisitResult.CONTINUE :继续遍历
  • FileVisitResult.SKIP_SUBTREE :跳过当前目录的所有子文件
  • FileVisitResult.SKIP_SIBLINGS :跳过当前目录下的其他文件和子目录
  • FileVisitResult.TERMINATE :立即终止遍历

SimpleFileVisitor

SimpleFileVisitor<T> 简化了 FileVisitor,是 FileVisitor<T> 的 默认实现,已提供基本方法,只需重写需要的方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SimpleFileVisitorDemo extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("访问文件: " + file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("进入目录: " + dir);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.out.println("无法访问文件: " + file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
System.out.println("退出目录: " + dir);
return FileVisitResult.CONTINUE;
}

public static void main(String[] args) throws IOException {
Path startPath = Paths.get("testDir");
Files.walkFileTree(startPath, new SimpleFileVisitorDemo());
}
}

运行程序,控制台打印如下:

1
2
3
4
5
6
7
makefile复制编辑进入目录: testDir
访问文件: testDir/file1.txt
访问文件: testDir/file2.txt
进入目录: testDir/subDir
访问文件: testDir/subDir/file3.txt
退出目录: testDir/subDir
退出目录: testDir

场景示例

递归删除目录(支持非空目录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DeleteDirectoryVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file); // 删除文件
System.out.println("删除文件: " + file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); // 删除目录
System.out.println("删除目录: " + dir);
return FileVisitResult.CONTINUE;
}

public static void main(String[] args) throws IOException {
Path dirToDelete = Paths.get("testDir");
Files.walkFileTree(dirToDelete, new DeleteDirectoryVisitor());
}
}

作用:删除非空目录,Files.delete() 直接删除空目录,但 FileVisitor 可递归删除所有文件和子目录。

统计目录中的文件大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FileSizeCalculator extends SimpleFileVisitor<Path> {
private long totalSize = 0;

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
totalSize += attrs.size(); // 累加文件大小
return FileVisitResult.CONTINUE;
}

public long getTotalSize() {
return totalSize;
}

public static void main(String[] args) throws IOException {
Path path = Paths.get("testDir");
FileSizeCalculator calculator = new FileSizeCalculator();
Files.walkFileTree(path, calculator);
System.out.println("目录总大小: " + calculator.getTotalSize() + " 字节");
}
}

作用:递归计算目录的总大小,包括所有子目录的文件大小。

复制目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CopyDirectoryVisitor extends SimpleFileVisitor<Path> {
private Path sourceDir;
private Path targetDir;

public CopyDirectoryVisitor(Path sourceDir, Path targetDir) {
this.sourceDir = sourceDir;
this.targetDir = targetDir;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path newDir = targetDir.resolve(sourceDir.relativize(dir));
Files.createDirectories(newDir); // 复制目录
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path newFile = targetDir.resolve(sourceDir.relativize(file));
Files.copy(file, newFile, StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}

public static void main(String[] args) throws IOException {
Path source = Paths.get("sourceDir");
Path target = Paths.get("backupDir");
Files.walkFileTree(source, new CopyDirectoryVisitor(source, target));
System.out.println("目录复制完成");
}
}

作用:递归复制目录,Files.copy() 只能复制单个文件,而 FileVisitor 可遍历所有子目录进行复制。

WatchService

概述

文件监听服务 WatchService 是 Java NIO 提供的文件监听服务,用于监视目录中的文件变化,如:

  • 文件创建(ENTRY_CREATE
  • 文件修改(ENTRY_MODIFY
  • 文件删除(ENTRY_DELETE

适用场景

  • 实时文件监控
  • 日志文件自动更新
  • 自动重载配置
  • 检测文件上传(FTP/云存储)
  • 开发工具(如 IDE)监听文件变更

工作流程

  1. 创建 WatchService
  2. 注册目录到 WatchService
  3. 监听事件
  4. 处理触发的文件事件

核心 API

方法 作用
FileSystem.newWatchService() 创建 WatchService
Path.register(WatchService, WatchEvent.Kind<?>...) 将目录注册到 WatchService
watchService.poll() 非阻塞地获取事件
watchService.take() 阻塞等待事件
watchKey.pollEvents() 获取事件列表
watchKey.reset() 重新注册监听事件

监听目录变化示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class WatchServiceExample {
public static void main(String[] args) throws IOException, InterruptedException {
// 1. 获取文件系统的 WatchService
WatchService watchService = FileSystems.getDefault().newWatchService();

// 2. 监听的目录(确保该目录存在)
Path dir = Paths.get("watchedDir");
if (!Files.exists(dir)) {
Files.createDirectory(dir);
}

// 3. 注册监听事件
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

System.out.println("正在监听目录: " + dir);

// 4. 监听事件(无限循环)
while (true) {
WatchKey key = watchService.take(); // 阻塞等待事件

for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path filePath = dir.resolve((Path) event.context());

System.out.println("事件类型: " + kind + ",文件: " + filePath);
}

boolean valid = key.reset();
if (!valid) {
break;
}
}

watchService.close();
}
}

运行程序,当 watchedDir 目录下有文件变化时,会输出:

1
2
3
4
正在监听目录: watchedDir
事件类型: ENTRY_CREATE,文件: watchedDir/newFile.txt
事件类型: ENTRY_MODIFY,文件: watchedDir/newFile.txt
事件类型: ENTRY_DELETE,文件: watchedDir/newFile.txt

拓展

  • 可以注册多个目录进行监听:

    1
    2
    3
    4
    5
    Path dir1 = Paths.get("dir1");
    Path dir2 = Paths.get("dir2");

    dir1.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
    dir2.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
  • 如果有必要,还可以搭配线程池进行异步监听:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
    while (true) {
    try {
    WatchKey key = watchService.take();
    for (WatchEvent<?> event : key.pollEvents()) {
    System.out.println("事件类型: " + event.kind() + ",文件: " + dir.resolve((Path) event.context()));
    }
    if (!key.reset()) {
    break;
    }
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    break;
    }
    }
    });

    异步监听不会阻塞主线程,适用于后台运行的文件监听程序。

监听事件类型

WatchService 可监听三种文件事件:

事件类型 描述
ENTRY_CREATE 文件或目录被创建
ENTRY_MODIFY 文件被修改(写入、更新)
ENTRY_DELETE 文件或目录被删除

监听子目录

WatchService 默认不会监听子目录,如果需要递归监听子目录,可以使用 FileVisitor 注册所有子目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class RecursiveWatchService {
public static void main(String[] args) throws IOException {
WatchService watchService = FileSystems.getDefault().newWatchService();
Path rootDir = Paths.get("watchedDir");

if (!Files.exists(rootDir)) {
Files.createDirectories(rootDir);
}

// 递归注册所有子目录
Files.walkFileTree(rootDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
return FileVisitResult.CONTINUE;
}
});

System.out.println("监听目录及子目录: " + rootDir);

while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println("事件类型: " + event.kind() + ",文件: " + key.watchable());
}
if (!key.reset()) {
break;
}
}
}
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!