程序要操作本地操作系统的一个文件,可以分为以下三个部分:
- 对文件位置的操作
- 对文件的操作
- 对文件内容的操作
其中,对文件内容的操作在 Java NIO之Channel 中已经有了介绍,通过FileChannel可以读/写文件内容。本文不做重复介绍,详情参考我的另一篇文章: Java NIO之Channel 。
1. 对文件位置的操作
在java.io中,有一个File类可以对文件位置、文件进行操作,对这两种操作没有区分开。对于操作文件位置的能力来说,File类不能很好地遍历目录,对于文件位置的操作非常不灵活,只能从父目录开始,一层一层地访问。
在java.nio中,提供了一个Path接口,这个接口专门处理文件位置,对文件本身操作不做处理。这个Path所代表的位置可以真实存在,也可以暂时先不存在。如果这个位置不存在,试图对文件、文件内容进行操作的时候,就会报错;但是对文件位置进行操作没有任何问题。
1.1 获取Path实例
Path是一个接口,不能直接实例化,最常用的实例化方法如下:
Path listing = Paths.get("Y:/path/usecase/example.txt");
对于Path的实例化,可以用绝对路径,也可以用相对路径,相对路径是指相对于代码运行时所在位置。如果文件位置在程序外的固定位置,就应该用绝对路径。如果文件位置在程序内,并且程序的运行时位置可能会变动,就应该用相对位置。
1.2 获取Path信息
Path接口提供了获取当前文件/目录名、父目录、根目录、子目录以及Path的元素个数:
Path listing = Paths.get("Y:/path/usecase/example.txt");
System.out.println("文件名称:"+listing.getFileName());
System.out.println("这个Path的元素个数:"+listing.getNameCount());
System.out.println("父路径:"+listing.getParent());
System.out.println("根路径:"+listing.getRoot());
System.out.println("从根路径开始数起,2个元素深度的子路径:"+listing.subpath(0, 2));
文件名称:example.txt
这个Path的元素个数:3
父路径:Y:\path\usecase
根路径:Y:\
从根路径开始数起,2个元素深度的子路径:path\usecase
如果你想通过当前位置,找到一个相对位置的文件,就非常方便,这在java.io中是做不到的。
1.3 找到Path的真实路径
首先,加入创建Path实例的时候,用的是相对路径,有时间我们需要知道绝对路径在哪:
Path listing1 = Paths.get("example1.txt");
System.out.println("相对路径:"+listing1);
System.out.println("绝对路径:"+listing1.toAbsolutePath());
然后有时候我们会遇到用一个点代表当前目录的写法,这个写法在Path构建中是没有必要的。“./example2.txt”和“example2.txt”实际上代表的意思一样。遇到这种情况,在Path构建中就必须先正常化:
Path listing2 = Paths.get("./example1.txt");
System.out.println("相对路径:"+listing2);
System.out.println("绝对路径:"+listing2.toAbsolutePath());
Path listing3 = Paths.get("./example1.txt").normalize();
System.out.println("相对路径:"+listing3);
System.out.println("绝对路径:"+listing3.toAbsolutePath());
相对路径:.\example2.txt
绝对路径:Y:\item\workspaces\workspace-mp\alijishi\.\example2.txt
相对路径:example3.txt
绝对路径:Y:\item\workspaces\workspace-mp\alijishi\example3.txt
在Linux系统中,还有一种符号链接。比如/usr/logs只是一个符号链接,真实的位置在/application/logs,这个位置才是日志文件真正所在的位置。如何通过符号链接找到真实位置呢?通过toRealPath方法:
Path listing4 = Paths.get("example4.txt").toRealPath(options...);
1.4 对Path的合并和比较
Path prefix = Paths.get("Y:/prefix");
Path completePath = prefix.resolve("example4");
System.out.println("完整路径:"+completePath);
boolean start = completePath.startsWith("Y:/prefix");
System.out.println("completePath是否以Y:/prefix开头:"+start);
boolean end = completePath.endsWith("example3");
System.out.println("completePath是否以example3结尾:"+end);
Path listing5 = Paths.get("Y:/prefix/example5");
Path relativizePath = completePath.relativize(listing5);
System.out.println("completePath和listing5的相对位置:"+relativizePath);
完整路径:Y:\prefix\example4
completePath是否以Y:/prefix开头:true
completePath是否以example3结尾:false
completePath和listing5的相对位置:..\example5
1.5 在目录中查找文件
Path listing6 = Paths.get("Y:/path/usecase");
try(DirectoryStream<Path> stream = Files.newDirectoryStream(listing6,"*.txt")) {
for(Path path:stream) {
System.out.println(path);
}
}
Y:\path\usecase\example.txt
DirectoryStream的第一个参数是目录位置,第二个参数是模式匹配(glob模式匹配),查询所有txt文件并打印出来。
1.6 遍历目录树
Path listing7 = Paths.get("Y:/path/usecase");
Files.walkFileTree(listing7, new PrintFileVisitor());
private static class PrintFileVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(file);
Objects.requireNonNull(attrs);
System.out.println(file);
return FileVisitResult.CONTINUE;
}
}
Y:\path\usecase\example.txt
Y:\path\usecase\mu1\example.txt
Y:\path\usecase\mu1\mu11\example.txt
Y:\path\usecase\mu1\mu12\example.txt
Y:\path\usecase\mu2\example.txt
Y:\path\usecase\mu3\example.txt
Y:\path\usecase\mu3\mu31\example.txt
Y:\path\usecase\mu3\mu32\example.txt
Y:\path\usecase\mu3\mu33\example.txt
Y:\path\usecase\mu3\mu33\mu331\example.txt
在java.nio中,有一个FileVisitor
- FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) 访问上层目录
- FileVisitResult visitFile(T file, BasicFileAttributes attrs) 访问当前目录文件
- FileVisitResult visitFileFailed(T file, IOException exc) 访问文件失败怎么处理
- FileVisitResult postVisitDirectory(T dir, IOException exc) 访问下层目录
java.nio提供了一个默认实现SimpleFileVisitor
2 对文件的操作
上面介绍的Path接口只是对文件位置进行操作,java.nio还提供了一个Files工具类,专门操作文件。可以取代java.io中的File类。
2.1 创建和删除文件
创建文件的时候还可以指定文件属性:
Path listing8 = Paths.get("Y:/path/usecase/created.txt");
//UNIX系统
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-rw-rw-");
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
Files.createFile(listing8,attr);
Files.delete(listing8);
2.2 文件复制和移动
Path listing9 = Paths.get("Y:/path/usecase/example.txt");
Path listing10 = Paths.get("Y:/path/usecase/copy/example.txt");
Files.copy(listing9,listing10,StandardCopyOption.COPY_ATTRIBUTES,StandardCopyOption.REPLACE_EXISTING);
移动文件的时候,有三个选项可以配置:
- COPY_ATTRIBUTES是指复制文件的时候把文件属性也一起复制过去;
- ATOMIC_MOVE是指确保两边操作都成功,否则回滚。在复制的时候用不着;
- REPLACE_EXISTING是指如果目标文件存在该文件,就覆盖。
2.3 文件属性
每个文件都有它的属性,比如访问权限控制、修改时间、文件大小、文件类型、是不是目录、是不是符号链接……
文件属性分为通用属性和操作系统专有属性。通用属性在java.nio有一个BasicFileAttributes接口,通过以下代码可以获取:
BasicFileAttributes attr = Files.readAttributes(listing9, BasicFileAttributes.class);
在Files类中,对每个通用属性都提供了对应的获取方法,这里不一一探讨。
对于操作系统专有属性,java.nio允许操作系统开发者提供实现FileAttributeView接口和BasicFileAttributes接口,像PosixFileAttributes就是Unix系统专有的属性,这里不做探讨。但是设置文件属性的时候一定要注意系统的兼容性。
2.4 监控文件的变化
在java.nio中,提供了一个WatchService接口,WatchService的实例可以监控文件或目录的变化,监控到变化的时候就会返回一个事件。这在很多重要场景中使用,比如对应用的环境变量监控。
try {
WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Paths.get("Y:/path/usecase");
//检测变化
WatchKey key = dir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
while(true) {
key = watcher.take();
for(WatchEvent<?> event:key.pollEvents()) {
if(event.kind()==StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("dir changed!");
}
}
}
} catch (Exception e) {
}
这个也是用观察者模式来实现。监控器WatchKey是个阻塞对象,可以监控多个目录/文件的多个不同事件类型。有点类似于网络IO中的Selector,也是个IO多路复用。下面是可以监控的四种事件:
- ENTRY_CREATE 创建事件
- ENTRY_DELETE 删除事件
- ENTRY_MODIFY 修改事件
- OVERFLOW 磁盘不够事件
3.总结
本文并没有把相关的类和方法全部覆盖,重点讲了Path和Files的用法,他们分别处理文件位置和文件。至于文件内容的操作超出了本文的范围。掌握了这三个部分的操作,就完整地掌握了对文件的操作。