JDK7提出了NIO2文件系统API,存取了默认文件系统进行各种输入/输出的API,既可简化现有的文档输入/输出API操作,也增加了许多过去没有提供的文件系统存取功能。
NIO2架构
在JDK7出现之前,常要针对特定文件系统攥写特定程序,不仅攥写方式没有标准,针对特定功能攥写程序也会增加应用程序开发者的负担。
NIO2文件系统API提供一组标准接口和类,应用程序开发者只要基于这些标准接口与类进行文件系统操作,底层实际如何进行文件系统操作,是由文件系统提供者负责。
应用程序开发者主要使用java.nio.file与java.nio.file.attribute, 包中必须操作的抽象类或接口,由文件系统提供者操作,只有文件系统提供者才关心java.nio.file.spi包。
NIO2文件系统的中心是java.nio.file.spi.FileSystemProvider,本身为抽象类,是文件系统提供者才要操作的类,作用是产生java.nio.file与java.nio.file.attribute中各种抽象类或接口的操作对象。
下图是FileSystemProvider产生的各种操作对象:
对于应用程序开发者,只需知道有这个东西的存在即可。应用程序开发者可以通过一些类提供的静态方法,取得相关操作对象或进行各种文件系统操作,这些静态方法内部会运用FileSystemProvider来取得所需的操作对象,完成应有的操作。
例如想要取得java.nio.file.FileSystem操作对象,可以通过FileSystems.getDefault()来取得:
FileSystem fileSystem = FileSystems.getDefault();
操作路径
想要操作文档,就得先指出文档路径。Path实例是在JVM中路径的代表对象,也是NIO2文件系统API操作的起点,NIO2文件系统API中有许多操作,都必须使用Path指定路径。
想要取得Path实例,可以使用Paths.get()方法。最基本的使用方式,就是使用字符串路径,可以使用绝对路径,也可以使用相对路径。例如:
Path workspace = Paths.get("C:\\workspace");
Path books = Paths.get("Desktop\\books");
Path path = Paths.get(System.getProperty("user.home"), "Documents", "Downloads");
Paths.get()的第二个参数开始接受不定长度自变量,因此可以指定起始路径,之后的路径分段指定。
如果用户目录是C:\Users\Justin, 那么以上第三个Path实例代表的路径就是C:\Users\Justin\Documents\Downloads。
Path实例仅代表路径信息,该路径实际对应的文档或文件夹不一定存在。Path提供一些方法取得路径的各种信息。例如:
public class PathDemo {
public static void main(String[] args){
Path path = Paths.get(System.getProperty("user.home"), "git", "Linux");
out.printf("toString: %s\n", path.toString());
out.printf("getFileName: %s\n", path.getFileName());
out.printf("getName(0): %s\n", path.getName(0));
out.printf("getNameCount: %d\n", path.getNameCount());
//Path.subpath(int a, int b)是用来将a,b-1之间的部分进行截取
out.printf("subpath(0, 2): %s\n", path.subpath(0, 2));
out.printf("getParent: %s\n", path.getParent());
out.printf("getRoot: %s\n", path.getRoot());
}
}
路径元素是以文件夹为单位的,最上层文件夹为索引0。
Path操作了Iterable接口,若要循序取得Path中分段的路径信息,也可以使用增强式for循环或JDK8新增的forEach(out::println);
Path的toAbsolutePath()方法可以将相对路径转为绝对路径,如果路径是符号链接,使用toRealPath方法可以转为真正的路径,若是相对路径则转换为绝对路径,若路径中由冗余信息也会移除。
路径与路径可以使用resolve方法进行结合。
如果想知道如何从一个路径切换至另一个路径,则可以使用relativize方法。
equals方法比较两个Path路径是否相同,startsWith方法比较路径起始是否相同,endsWith方法比较路径结尾是否相同,如果文件系统支持符号链接,那么就算路径不同也有可能指的是同一文档,我们可以使用Files.isSameFile方法判断。
可以使用Files.exists方法判断路径所指的文档是否存在,如果存在就返回true,如果不存在或无法读取(没有存取权限)会返回false,Files.notExists方法和它刚好相反。
属性读取与设定
在过去并没有标准的方式取得不同文件系统支持的不同属性,在JDK7中,可以通过BasicFileAttributes,DosFileAAttributes,PosixFileAttributes针对不同的文件系统取得支持的属性,后两个接口继承自第一个接口。
public class BasicFileAttributesDemo {
public static void main(String[] args) throws IOException{
Path path = Paths.get("/home/paranoid/git/Linux");
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
out.printf("creationTime: %s\n", attributes.creationTime());
out.printf("lastAccessTime: %s\n", attributes.lastAccessTime());
out.printf("lastModifiedTime: %s\n", attributes.lastModifiedTime());
out.printf("isDirectory: %b\n", attributes.isDirectory());
out.printf("isOther: %b\n", attributes.isOther());
out.printf("isRegularFile: %b\n", attributes.isRegularFile());
out.printf("isSymbolicLink: %b\n", attributes.isSymbolicLink());
out.printf("size: %d\n", attributes.size());
}
}
creationTime(),lastAccessTime(),lastModifiedTime()返回的是FileTime实例,也可以通过getLastModifiedTime()取得最后修改的时间。
属性设定主要通过Files.setAttribute方法。
Files.setAttribute方法的第二个自变量必须指定FileAttributeView子接口规范的名称,格式为[view-name:]attribute-name。view-name可以从FileAttributeView子接口操作对象的name()方法取得,如果省略就默认为“basic”,attribute-name可在FileAttributeView各子接口的API文件中查询。如:同样设定最后修改时间,Files.setAttribute方法可以这样攥写:
long currentTime = System.currentTimeMillis();
FileTime filetime = FileTime.fromMillis(currentTime);
Files.setAttribute(Paths.get("/home/paranoid/git"), "basic: lastModifiedTime", filetime);
DosFileAAttributes,PosixFileAttributes相关API有兴趣的同学可以下去了解。
如果想要取得存储装置本身的信息,可以利用Files.getFileStore方法取得指定路径的FileStore 实例。通过FileSystem的getFileStores方法取得所有存储装置的FileStore实例,如:
public class Disk {
public static void print(FileStore fileStore) throws IOException{
long total = fileStore.getTotalSpace();
long used = fileStore.getUnallocatedSpace();
long usable = fileStore.getUsableSpace();
DecimalFormat format = new DecimalFormat("#, ###, ###");
out.println(fileStore.toString());
out.printf("总容量: %s字节\n", format.format(total));
out.printf("可用空间: %s字节\n", format.format(used));
out.printf("已用空间: %s字节\n", format.format(usable));
}
public static void main(String[] args) throws IOException{
if(args.length == 0){
FileSystem fileSystem = FileSystems.getDefault();
//通过getFileStores方法取得所有存储装置FileStore实例
for(FileStore store : fileSystem.getFileStores()){
print(store);
}
}else{
for(String file : args){
FileStore fileStore = Files.getFileStore(Paths.get(file));
print(fileStore);
}
}
}
}
FileSystem的getFileStores方法会以iterable返回所有存储装置的FileStore对象。
操作文档与目录
如果想要删除Path代表的文档或目录,可以使用Files.delete方法,如果不存在,会抛出NoSuchFileException,如果因为目录不为空而无法删除文档,会抛出DirectoryNotEmptyException。使用Files.deleteIfExists方法删除文档时,如果文档不存在,并不会抛出异常。
如果想要复制来源Path的文档或目录至目的地Path,可以使用Files.copy(),这个方法的第三个参数可以指定CopyOption接口的操作对象。具体的希望大家可以下去了解,举个例子:
Path src = ...;
Path dest = ...;
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
Files.copy()还有两个重载版本。一个是接受InputStream作为来源,可直接读取数据,并将结果复制在指定的Path中,一个是将源Path复制至指定的OutputStream。例如:
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
public class NIO2Download {
public static void main(String[] args) throws IOException{
URL url = new URL(args[0]);
//REPLACE_EXISTING如目标文档存在就会覆盖
Files.copy(url.openStream(), Paths.get(args[1]), REPLACE_EXISTING);
}
}
若要进行文档或目录的移动,可以使用Files.move()方法,使用方式与Files.copy()方法类似,如果文件系统支持原子移动,可在移动时指定StandardCopyOption.ATOMIC_MOVE选项。
如果要建立目录,可以使用Files.createDirectory()方法,如果调用时父目录不存在,会抛出NoSuchFileException。Files.createDirectories()会在父目录不存在时一并建立。
如果要建立暂存目录,可以使用Files.createTempDirectory(),这个方法有可指定路径与使用默认路径建立暂存目录两个版本。
如果文档都是字符,则需要在读取或写入时使用缓冲区,也可以使用Files.newBufferedReader(),Files.newBufferedWriter()指定文档Path和编码,他们分别会返回BufferedReader(),BufferedWriter实例,可以使用他们进行文档读取和写入。例如如果原先有个建立BufferedReader的片段如下:
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(args[0]), "utf-8"));
可使用Files.BufferedReader()改写如下:
BufferedReader reader = Files.newBufferedReader(Paths.get(args[0]), "utf-8");
如果想要以InputStream和OutputStream处理相关文档,也有相对应的Files.newInputStream和Files.newOutputStream。
读取,访问目录
如果想要获得文件系统根目录路径信息,可以使用FileSystem的getRootDirectories()方法,这回取回Iterable对象。例如:
public class Roots {
public static void main(String[] args){
Iterable<Path> iterable = FileSystems.getDefault().getRootDirectories();
iterable.forEach(out::println);
}
}
也可以使用Files.newDirectoryStream()方法取得DirectoryStream()接口操作对象,代表指定路径下的所有文档。
DirectoryStream可以使用尝试关闭资源语法。Files.newDirectoryStream()实际返回的是DirectoryStream<Path>。
下面这个范例可以从命令行自变量指定目录中,查询出该目录下的文档:
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import static java.lang.System.out;
public class Dir{
public static void main(String[] args) throws IOException{
//尝试自动关闭资源获取目录下的文档
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(Paths.get(args[0]))){
List<String> list = new ArrayList<>();
for(Path path : directoryStream){
if(Files.isDirectory(path)){
out.printf("[%s]\n", path.getFileName());
}else{
list.add(path.getFileName().toString());
}
}
list.forEach(out :: println);
}
}
}
如果想要访问目录中所有的文档和子目录,可以调用FileVisitor接口,其中定义了4个必须操作的方法,如果只对其中的一两个方法有兴趣,可以继承SimpleFileVisitor类,这个类操作了FileVisitor接口,只要继承之后重新定义感兴趣的方法就可以了。
从指定的目录路径开始,每次要访问该目录的内容前,都会调用previsitDirectory(),要访问文档时会调用visitFile(),访问文档失败会调用visitFileFailed(),访问整个目录内容后会调用postVisitDirectory(),如果有多层目录,如下图:(递归)
可以使用Files.walkFileTree()访问目录,例如:
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import static java.lang.System.err;
import static java.lang.System.out;
import static java.nio.file.FileVisitResult.CONTINUE;
public class ConsoleFileVisitor extends SimpleFileVisitor<Path>{
@Override
public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attributes)
throws IOException{
printSpace(path);
out.printf("[%s]\n", path.getFileName());
return CONTINUE;
}
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attributes){
printSpace(path);
out.printf("%s\n", path.getFileName());
return CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc){
err.println(exc);
return CONTINUE;
}
private void printSpace(Path path){
out.printf("%" + path.getNameCount()*2 + "s", "");
}
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class DirAll {
public static void main(String[] args) throws IOException{
Files.walkFileTree(Paths.get(args[0]), new ConsoleFileVisitor());
}
}
在NIO2的目录访问上,新增了list(),walk(),list方法显示当前目录下的所有文档,walk方法显示当前目录下及子目录下所有的文档。都可以使用尝试自动关闭资源。
try(Stream<Path> paths = Files.list(Paths.get(args[0]))){
paths.forEach(out::println);
}
过滤,搜索文档
如果想在列出目录内容时过滤想显示的文档,例如只想显示.class和.jar文档,就可以使用Files.newDirectoryStream()时的第二个参数指定过滤条件为*.{class,jar}。例如:
try(DirectoryStream<path> directoryStream = Files.newDirectoryStream(Paths.get(args[0]), "*.{class, jar}")){
directoryStream.forEach(path -> out.println(path.getFilename));
}
上面的*.{class, jar}是Glob语法,比正则表达式更简单,常用于目录与文件名的比较。有兴趣的同学可以多了解一下。
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.lang.System.out;
public class Ls {
public static void main(String[] args) throws IOException{
//默认取得所有文档
String glob = args.length == 0 ? "*" : args[0];
//获取当前工作目录
Path uerPath = Paths.get(System.getProperty("user.dir"));
try(DirectoryStream<Path> directoryStream = Files.newDirectoryStream(uerPath, glob)){
directoryStream.forEach(path -> out.println(path.getFileName()));
}
}
}
如果启动JVM时指定命令行自变量为build*,表示使用Glob语法为build*,那么工作目录下所有build开头的文档或目录都会显示出来。
使用FileSystem实例的getPathMatcher()取得PathMatcher接口操作对象,在取得PathMatcher时可以指定使用那种模式比较语法,“regex”表示使用规则表达式,“glob”表示使用glob语法。例如:
PathMatcher pathMatcher = FileSystem.getDefault().getPathMatcher("glob:*.{class, jar}");
取得PathMatcher之后,可以使用matches方法进行路径比较,返回true表示符合模式,来看一个范例:
import java.io.IOException;
import java.nio.file.*;
import static java.lang.System.out;
public class Ls2 {
public static void main(String[] args) throws IOException{
//默认使用glob模式
String syntax = args.length == 2? args[0] : "glob";
String pattern = args.length == 2? args[1] : "*";
out.println(syntax + ":" + pattern);
//取得当前的工作目录
Path userPath = Paths.get(System.getProperty("user.dir"));
//按照输入方式进行搜索
PathMatcher pathMatcher = FileSystems.getDefault().
getPathMatcher(syntax + ":" + pattern);
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(userPath)){
directoryStream.forEach(path -> {
Path file = Paths.get(path.getFileName().toString());
if(pathMatcher.matches(file)){
out.println(file.getFileName());
}
});
}
}
}