Paths 类与 Files 类属于 NIO 2 的一部分,在 JDK 7 中引入,并在 JDK 8 中增加了一些适应新增特性(Stream等)的方法。这两个类一般配合起来使用,其中封装了许多操作文件的方法,相比之前的 File 类,更为简洁方便,也包含更细粒度的控制功能。
关于 File 类存在的问题,可以参考这篇文章:Legacy File I/O Code
Path
Path 可以理解为定义的一个路径,对应一个文件或目录。该文件或目录如果已经存在,则可以通过此Path 进行文件的修改、删除、复制等操作,否则需要先创建该文件再进行后续操作。
创建 Path 的三种方式:
- 通过 Paths 的 get() 方法,该方法的参数可以是 String 或 URI
Path path = Paths.get(“d:/test.txt”);
// 如果 String 过长,可以将其分成多个 String 表示
Path lPath = Paths.get(“d:”, “test.txt”);
- 通过 FileSystem 的 getPath() 方法,FileSystems.getDefault() 方法返回默认的文件系统供 JVM 访问
Path path = FileSystems.getDefault().getPath(“d:”, “test.txt”);
- 通过 File 转化
File file = new File(“d:”, “test.txt”);
Path path = file.toPath();
文件操作(Files)
- 创建与删除文件/目录
//创建文件
Path path = Paths.get("d:/test.txt");
Files.createFile(path);
//创建目录
Path directory = Paths.get("d:", "pictures");
Files.createDirectory(directory);
删除有两种方法:
public static void delete(Path path) throws IOException
public static boolean deleteIfExists(Path path) throws IOException
两者的区别在于当文件不存在时,前者会抛出 NoSuchFileException 异常,后者会返回 false。
注意:当删除目录时,无论使用哪种删除方法,都只能删除空目录,否则会抛出 DirectoryNotEmptyException 异常。
- 文件复制
public static Path copy(Path source, Path target, CopyOption… options)
示例如下:
Path source = Paths.get("d:/test.txt");
Path target = Paths.get("e:/");
Files.copy(source, target.resolve(source.getFileName()),
StandardCopyOption.REPLACE_EXISTING);
resolve() 方法用来将两个路径合在一起组成新的路径,以上例来说,它的作用是将 d 盘下的 test.txt文件复制到 e 盘下,文件名保持一致。
StandardCopyOption 有如下类型:
- REPLACE_EXISTING:替换已存在的文件
- COPY_ATTRIBUTES:将源文件的文件属性信息复制到目标文件中
- ATOMIC_MOVE:原子性复制,即不存在因失败导致文件复制不完整的情况出现
注意:使用 copy 方法复制目录时只会复制空目录,目录中的文件不会复制。复制目录需要通过文件遍历来实现。
- 文件移动/重命名
// 文件移动与重命名用到的是同一个命令
public static Path move(Path source, Path target, CopyOption… options)
示例如下:
Path source = Paths.get("d:/test.txt");
Path target = Paths.get("e:/");//注意要有"/",否则会使用当前工作目录
//移动到其它目录
Files.move(source, target.resolve(source.getFileName()),
StandardCopyOption.REPLACE_EXISTING);
//重命名文件
Files.createFile(source);
Files.move(source, source.resolveSibling("test2.txt"),
StandardCopyOption.REPLACE_EXISTING);
resolveSibling() 方法的作用与 resolve() 方法类似,但它是以 source 的父目录与方法中的参数进行合并,例如 source.resolveSibling(“test2.txt”) 代表的路径就是 “d:/test2.txt”。
注意:当移动目录时,只能移动空目录,否则会抛出 DirectoryNotEmptyException 异常;给目录重命名时没有此限制。
- 文件读写
Path path = Paths.get("d:/test.txt");
Files.createFile(path);
BufferedWriter bw = Files.newBufferedWriter(path,
StandardCharsets.UTF_8,
StandardOpenOption.WRITE);
bw.write("Hello");
bw.newLine();
bw.write("World!");
bw.flush();
bw.close();
在文件读取时,有以下两种方法:
//读文件方式1(jdk1.7)
BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8);
String str;
while ((str = reader.readLine()) != null) {
System.out.println(str);
}
reader.close();
//读文件方式2(jdk1.8)
List<String> list = Files.readAllLines(path);
System.out.println(list);
- 文件遍历
- 非递归遍历的两种方法
Path dir = Paths.get("E:/Pictures");
//非递归遍历目录方式1(jdk1.7)
DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir);
for (Path item : directoryStream)
System.out.println(item.toAbsolutePath());
directoryStream.close();
//非递归遍历目录方式2(jdk1.8)
Stream<Path> stream = Files.list(dir);
stream.forEach(System.out::println);
stream.close();
- 递归遍历(深度优先遍历)的两种方法
//深度优先遍历方式1(jdk1.7)
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
//访问文件时调用
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
System.out.println(file.toAbsolutePath());
return FileVisitResult.CONTINUE;
}
//访问目录前调用
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException {
System.out.println(dir.toAbsolutePath());
return FileVisitResult.CONTINUE;
}
});
//深度优先遍历方式2(jdk1.8)
Stream<Path> deepStream = Files.walk(dir);
deepStream.forEach(System.out::println);
stream.close();
可以看到,借助于 JDK 8 中引入的 Stream 特性,语法更为简洁易懂。
- 获取与修改文件属性
public static Map<String,Object> readAttributes(Path path, String attributes, LinkOption… options)
示例如下:
Path path = Paths.get("d:/test.txt");
System.out.println(Files.readAttributes(path, "*"));
输出结果:
{lastAccessTime=2017-11-15T12:57:07.681984Z, lastModifiedTime=2017-11-15T12:57:07.681984Z,
size=0, creationTime=2017-11-15T12:57:07.681984Z, isSymbolicLink=false,
isRegularFile=true, fileKey=null, isOther=false, isDirectory=false}
也可以通过 setAttributes() 方法对文件属性进行修改:
public static Path setAttribute(Path path, String attribute, Object value, LinkOption… options)
示例如下:
Path path = Paths.get("d:/test.txt");
//更改文件属性方式1(Windows)
DosFileAttributeView view = Files.getFileAttributeView(path, DosFileAttributeView.class);
view.setArchive(false);
//更改文件属性方式2(Windows)
Files.setAttribute(path, "dos:archive", true);
方式1的好处是不需要记文件属性名,方式2的好处是简洁易懂,但前提是需要知道准确的文件属性名。
注意:由于各个操作系统文件属性并不相同,Windows 下使用的是 DosFileAttributeView;如果是 Linux 操作系统,使用的是 PosixFileAttributeView。
- 寻找文件
public static Stream
find(Path start, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption… options)
以寻找后缀为 “docx” 的文件为例:
Path dir = Paths.get("E:/Pictures");
Stream<Path> result = Files.find(dir, 2, (p, basicFileAttributes) -> String.valueOf(p).endsWith(".docx"));
result.map(Path::getFileName).forEach(System.out::println);
也可以在 walk() 方法返回的 Stream 上调用过滤器 filter 来实现此功能。但 find() 方法可能更有效,因为避免了重复检索 BasicFileAttributes。
- 文件监视 WatchService
在 NIO 2 中也提供了文件监视机制,可以对文件的创建、修改和删除事件进行监视。监视服务的具体实现依赖于操作系统,在可用的情况下会利用操作系统本地的文件监视功能。 但是,当操作系统不支持文件监视机制时,监视服务将采用轮询机制。
使用文件监视的步骤如下:
- 创建 WatchService 实例 watch
- 将需要监控的每个目录注册到观察器,返回 WatchKey 实例 key。在注册目录时,指定要通知的事件的类型
- 通过一个无限循环等待传入的事件。当事件发生时,key 发出信号,加入到 watch 的 queue
- 遍历 key 的事件并根据事件类型进行处理
- 重置 key,重新等待事件
- 关闭监视服务
public class WatchServiceDemo {
public static void main(String[] args) throws IOException, InterruptedException {
String path = "d:/watch";
watch(path);
}
public static void watch(String directory) throws IOException, InterruptedException {
Path path = Paths.get(directory);
WatchService watch = FileSystems.getDefault().newWatchService();
path.register(watch, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
while (true) {
WatchKey key = watch.take();
for (WatchEvent event : key.pollEvents()) {
if (event.kind() == ENTRY_CREATE) {
System.out.println("文件创建:" + event.context());
}
if (event.kind() == ENTRY_MODIFY) {
System.out.println("文件修改:" + event.context());
}
if (event.kind() == ENTRY_DELETE) {
System.out.println("文件删除:" + event.context());
}
}
boolean reset = key.reset();
if (!reset) {
System.out.println("监视服务终止!");
break;
}
}
watch.close();
}
}
注意:在创建、重命名文件以及修改文件内容时会监测到两个重复的事件。此外,上述代码只能监测到该目录下的文件事件,而无法监测到子目录中的文件事件(但能监测到子目录是否发生了变化)。
参考链接: