Java 输入与输出之 NIO.2【AIO】【Path、Paths、Files】【walkFileTree接口】探索之【三】
在JDK 1.7 版本中对NIO进行了完善,推出了NIO.2,也称为AIO(异步IO),在处理大量并发请求时具有优势,特别是在网络编程和高并发场景下,表现得更为出色。
对于输出流和输入流而言,操作的对象可以是文件,大多数情况下是文件,也可以是网络连接,当然也可以是内存数据。我们今天主要讨论的是文件和目录处理。
本博客我们将探讨NIO.2中最基础的Path、Paths和Files的用法。
Path 和 Files 是 Java NIO 中的两个核心类,分别代表文件系统中的路径和一组文件操作。
一、Path和Paths
(一)、File和Path的关系
早期的java只提供了一个File类来访问文件系统,但File类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异常信息。
NIO. 2为了弥补这种不足,引入了Path接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。可以表示文件或者文件目录。
Path 接口属于 java.nio 包中的最基础类,Path可以看做是java.io.File的升级版本。对 Path 接口而言,主要是为了在文件系统中定位文件的对象。通常情况下表示系统相关的文件路径。文件路径既包含文件也包含目录。
所以 Path 接口对象,可以是由一系列的目录和文件名元素组成的。并且是有一个有层次结构的路径。并且在文件路径中,还包含特殊的分隔符或定界符分割。当然也可以包含文件系统中的根目录。或者叫做盘符。对于文件而言,通常在文件路径中的最右侧。也就是最后一个名称中。其他的都是只能被称为目录。也就是说,目录中可以包含目录和文件。但是文件中却不能包含目录。
java.nio.file 包定义了访问文件和文件系统的类。
java.nio.file.attribute 包中定义是关于文件和文件系统的属性的 API 。
文件操作的二种常见写法:
在Java7以前文件的IO操作都是这样写的:
import java.io.File;
File file = new File(“test.txt”);
但在Java7 中,我们可以这样写:
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get(“test.txt”);
如果 Path 的路径由一个空的名字元素组成,则 Path 被认为是空路径。使用空路径访问文件相当于访问文件系统的默认目录。
Path常用方法:
boolean endsWith(String path) : 判断是否以 path 路径结束
boolean startsWith(String path) : 判断是否以 path 路径开始
boolean isAbsolute() : 判断是否是绝对路径
Path getFileName() : 返回与调用 Path 对象关联的文件名
Path getName(int idx) : 返回的指定索引位置 idx 的路径名称
int getNameCount() : 返回Path 根目录后面元素的数量
Path getParent() :返回Path对象包含整个路径,不包含 Path 对象指定的文件路径
Path getRoot() :返回调用 Path 对象的根路径
Path resolve(Path p) :将相对路径解析为绝对路径
Path toAbsolutePath() : 作为绝对路径返回调用 Path 对象
String toString() : 返回调用 Path 对象的字符串表示形式
(二)、Path 对象的获取:
Path是接口,不能通过new关键字直接去构建 实例,创建实例大致有四种方法:
1)根据字符串格式的路径获取:使用Paths工具类中的get方法去获取,方法的参数是1个或1个以上的String,Paths会自动根据参数中的字符串对路径做拼接。另外,两个点"…"表示的是路径向上回退一级。
2)根据URI获取:使用Paths的get(URI url)方法获取。
3)根据File的实例进行转换:File中定义了实例方法toPath(),可以将File实例转换为Path的实例。
4)从对其他Path实例的操作中获取:这个相关的方法就有很多了,例如Path自己的getParent()、normalize()等。
JDK 1.7的NIO.2还提供了Files、Paths工具类,Files包含了大量静态的工具方法来操作文件;
(三)、Paths的两个静态方法
Paths则包含了两个返回Path的静态工厂方法get() 方法用来获取 Path 对象:
static Path get(String first, String … more) : 用于将多个字符串串连成路径
static Path get(URI uri): 返回指定uri对应的Path路径
URI
资源网络地址URL:在互联网上,每一信息资源都有统一的网上地址,该地址称为URL(Uniform Resource Locator,统一资源定位器),它是互联网的统一资源定位标志,俗称“网络地址”。
URL是URI的一个子集,一般用于表示互联网上的资源网络地址。
URI可以直接作为参数传递给get()方法生成Path实例。
用URL转换成URI,再获取Path实例的用法:
URI resource = URI.create(“file:///Users/exampledir/examplefile.txt”);
Path path = Paths.get(resource.toURI());
请看实例,演示Paths这两个Path静态工厂方法get() 的用法:
package nio;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.net.URI;
public class PathGetExample {
public static void main(String[] args) {
// 创建一个URI对象,指向文件系统中的某个路径
//URI uri = URI.create("file:///Users/exampleuser/exampledir/examplefile.txt");
URI uri = URI.create("file:/D:/Temp/image/c.txt");
// 使用Path接口的get(URI uri)方法获取Path对象
Path path = Paths.get(uri); //用法一
// 打印获取到的Path对象
System.out.println("Path: " + path.toString());
path = Paths.get("D:/Temp/image/c.txt"); //用法二
// 打印获取到的Path对象
System.out.println("Path: " + path.toString());
}
}
对于文件的访问, 通常Path和 Files 类一起使用。
(三)、Files类常用方法
java.nio.file.Files 用于操作文件或目录的工具类。
Files常用方法:用于创建、复制、删除和移动
Path copy(Path src, Path dest, CopyOption … how) : 文件的复制
Path createDirectory(Path path, FileAttribute<?> … attr) : 创建一个目录
Path createFile(Path path, FileAttribute<?> … arr) : 创建一个文件
void delete(Path path) : 删除一个文件
Path move(Path src, Path dest, CopyOption…how) : 将 src 移动到 dest 位置
long size(Path path) : 返回 path 指定文件的大小
Files常用方法:用于文件读写
byte[] Files.readAllBytes(Path path):读取所有字节。
Path write(Path path, byte[] bytes):写入字节数组。
Files提供的简便方法适用于处理中等长度的文本文件
如果要处理的文件长度较大,或者二进制文件,那么还是应该使用经典的IO流或FileChannel来处理。
我们来看一个文件读写的演示程序:
package nio;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
public class ReadWriteFile {
public static void main(String[] args) throws Exception {
Charset charset = Charset.defaultCharset(); //获取系统环境的默认字符集名
System.out.println( charset ); //显示系统环境默认字符集名称
/*
Files提供的简便方法适用于处理中等长度的文本文件
如果要处理的文件长度较大,或者二进制文件,那么还是应该使用经典的IO流
*/
Path src = Paths.get("D:/temp/test.txt");
Path target = Paths.get("D:/temp/target.txt");
//将文件所有内容读入byte数组中
byte[] bytes = Files.readAllBytes(src); //传入Path对象
//字节数组可以根据字符集构建字符串
String content = new String(bytes, charset);
System.out.println(content); //显示文件内容
//也可以直接当作行序列读入
List<String> lines = Files.readAllLines(src, charset);
//相反,也可以写一个字符串到文件中,默认是覆盖
Files.write(target, content.getBytes(charset)); //传入byte[]
//追加内容,根据参数决定追加等功能
Files.write(target, content.getBytes(charset), StandardOpenOption.APPEND); //传入枚举对象,打开追加开关
//将一个行String的集合List lines写出到文件中
//Files.write(path, lines);
}
}
我的演示测试结果:
最后,文件target.txt内容如下,是test.txt内容的二倍,因为又追加多写了一遍。
Files常用方法:用于判断各种属性
boolean exists(Path path, LinkOption … opts) : 判断文件是否存在
boolean isDirectory(Path path, LinkOption … opts) : 判断是否是目录
boolean isExecutable(Path path) : 判断是否是可执行文件
boolean isHidden(Path path) : 判断是否是隐藏文件
boolean isReadable(Path path) : 判断文件是否可读
boolean isWritable(Path path) : 判断文件是否可写
boolean notExists(Path path, LinkOption … opts) : 判断文件是否不存在
请看一个测试例程,演示Files中一些常用方法:
package nio;
import static java.nio.file.StandardCopyOption.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FilesTest {
public static void copyFile(Path source, Path target)//复制
{
try {
//path1需要在物理磁盘上真实存在,path2不存在则自动创建、存在则替换原有文件
Files.copy(source,target, REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void moveFile(Path source, Path target) //移动
{
try {
//source需要在物理磁盘上真实存在
Files.move(source,target,ATOMIC_MOVE); //移动是原子操作
} catch (IOException e) {
e.printStackTrace();
}
}
public static boolean fileExists(Path path)//文件是否存在
{
boolean b=Files.exists(path, LinkOption.NOFOLLOW_LINKS);
System.out.println("文件是否存在? "+b);
return b;
}
public static void main(String[] args) {
Path path1=Paths.get("D:/temp/a.txt");
Path path2=Paths.get("D:/temp/b.txt");
Path path3=Paths.get("D:/temp/c.txt");
copyFile(path1, path2);
moveFile(path2, path3);
fileExists(path1);
}
}
Files常用方法: 用于操作目录和文件内容
SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连接,how 指定打开方式。
DirectoryStream newDirectoryStream(Path path) : 打开 path 指定的目录
InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象
OutputStream newOutputStream(Path path, OpenOption…how) : 获取 OutputStream 对象
请看一个例程,演示访问文件:
package nio;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import static java.nio.file.StandardOpenOption.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class FilesDemo {
/****
打开或创建文件,返回可访问的字节通道以访问该文件
打开一个目录,返回一个DirectoryStream以遍历目录中的所有条目
打开一个文件,返回输入流以从文件中读取
打开或创建文件,返回可用于向文件写入字节的输出流
***/
public static void main(String[] args) {
Path path1=Paths.get("D:/temp/a.txt");
Path path2=Paths.get("D:/temp");
Path path4=Paths.get("D:/temp/c.txt");
SeekableByteChannel seekChannel=null;
DirectoryStream<Path> ds =null;
InputStream inStream = null;
OutputStream os =null;
try {
//1、打开或创建文件,返回可访问的字节通道以访问该文件
//READ:表示对应的Channel是可读的
//WRITE:表示对应的Channel是可写的
//CREATE:如果要写出的文件不存在则创建;存在则忽略
//CREATE_NEW:如果要写出的文件不存在则创建;存在则抛出异常
//static SeekableByteChannel newByteChannel(Path path, OpenOption... options)
seekChannel=Files.newByteChannel(path1, READ,WRITE,CREATE);
//2、打开一个目录,返回一个DirectoryStream以遍历目录中的所有条目
//public static DirectoryStream<Path> newDirectoryStream(Path dir) throws IOException
ds=Files.newDirectoryStream(path2);
Iterator<Path> it=ds.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
//3、打开一个文件,返回输入流以从文件中读取
inStream = Files.newInputStream(path1,READ);
//4、打开或创建文件,返回可用于向文件写入字节的输出流
os=Files.newOutputStream(path4,WRITE);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (seekChannel!=null)
seekChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
(四)、Files的两种方法可进行目录的遍历操作
Files可获得可迭代的目录流进行遍历操作
1. 方式一
使用Files.newDirectoryStream。这个接口只打印第一层的文件和文件夹。它是采用File.listFiles来列举所有的第一层文件。另外,因为是打开了一个stream,需要在finally中close stream。比较好的是用户可以自己定义过滤条件,支持“*”等通配符。
使用这个方式Files可获得可迭代的目录流,传入一个目录Path,遍历子孙目录返回一个目录Path的Stream,注意这里所有涉及的Path都是目录而不是文件!
因此,Files类设计了newDirectoryStream(Path dir)及其重载方法,将生成Iterable对象(可用foreach迭代)。可用于遍历目录文件集合。它有三种重载形式:
1,static DirectoryStream
2,static DirectoryStream
3,static DirectoryStream
返回一个目录流 时,它内含一个实现了Iterable接口的目录信息的集合,
因此可用迭代器或foreach迭代,只是使用迭代器的时候要注意不能再递归调用另一个Iterator迭代器。
可以传入通配符glob参数,使用通配模式来过滤文件,注意glob参数是String类型。下面是样例代码:
try(DirectoryStream<Path> entries = Files.newDirectoryStream(dir, "*.java")) //
{
...
}
通配模式(glob模式)
所谓的通配模式是指可使用“正则表达式”来进行匹配。
1.星号 * 匹配路径组成部分包含0个或多个字符;例如 .java 匹配当前目录中的所有Java(扩展名为java)文件。
2.双星号 ** 匹配跨目录边界包含0个或多个字符;例如 **.java 匹配在所有子目录中的Java文件。
3.问号(?) 只匹配一个字符;例如 ???.java 匹配所有四个字符的Java文件,不包括扩展名。
4.[…] 匹配一个字符集合,可以用连线 [0-9] 和取反符 [!0-9];例如 Test[0-9A-F].java 匹配Testx.java,假设x是一个十六进制数字,[0-9A-F]是匹配单个字符为十六进制数字,比如B(十六进制不区分大小写)。
如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。
5.{…} 匹配大括号内由逗号隔开的多个可选项之中的一个;例如 .{java,class} 可匹配所有Java文件或类class文件。
6.\ 转义上述任意模式中的字符;例如 * 匹配所有子目录中文件名包含的文件,这里为 ** 转义,前面第一个“”是匹配0个或多个字符。
网友总结的通配模式的规则:
请看一个打印当前目录中文件和目录的例子:
只能打印当前目录中的文件和目录。
package nio;
import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PrintDirectory {
/***
* 打印当前目录中的文件和目录
* @param rootPath
* @throws IOException
* ***/
public static void printFileDirectory(String rootPath) throws IOException {
Path path = Paths.get(rootPath);
if (Files.notExists(path)) {
String msg = "目录不存在:"+path;
System.out.println(msg);
return;
}
File[] listFiles = path.toFile().listFiles();
for (File file : listFiles) {
System.out.println("文件 :" + file.getAbsolutePath());
}
}
/*** 打印当前目录中的文件和目录(用通配模式打印)
* @param rootPath
* @param glob
* @throws IOException
*/
public static void printDirectory(String rootPath, String glob) throws IOException {
Path path = Paths.get(rootPath);
if (Files.notExists(path)) {
String msg = "目录不存在:"+path;
System.out.println(msg);
return;
}
DirectoryStream<Path> newDirectoryStream = null;
try {
if (glob != null) {
newDirectoryStream = Files.newDirectoryStream(path, glob);
} else {
newDirectoryStream = Files.newDirectoryStream(path);
}
for (Path directory : newDirectoryStream) {
System.out.println("路径:" + directory.toString());
}
} finally {
if (newDirectoryStream != null) {
newDirectoryStream.close();
}
}
}
public static void main(String[] args) throws Exception {
String rootPath = "D:/temp";
String glob = "*.mp3";
printDirectory(rootPath, glob);
System.out.println("分隔线--------------------------------------");
printFileDirectory(rootPath);
}
}
测试结果图:
2. 方式二
使用Files.walkFileTree接口。这个接口遍历目录下所有的文件和文件夹。接口的参数需要传入FileVisitor,提供了文件、文件夹的操作接口。
FileVisitor接口介绍
第二个参数需要实现FileVisitor接口,重写这个接口的四个方法:preVisitDirectory、visitFile、visitFileFailed、postVisitDirectory。
这几个方法的返回值都是枚举类型:
CONTINUE:继续;
SKIP_SIBLINGS:继续遍历,不过忽略当前节点的兄弟节点,直接返回上一层继续遍历
SKIP_SUBTREE:继续遍历,忽略后代目录,但继续访问子文件
TERMINATE:终止遍历
下面是一个演示程序:调用Files类的walkFileTree方法,并传入一个FileVisitor接口类型的对象。可递归打印指定目录下的所有文件和目录信息。
package nio;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
public class WalkFileTreeDemo {
public static void printFileTree(String rootPath) throws IOException {
Path path = Paths.get(rootPath);
if (Files.notExists(path)) {
String msg = "目录不存在:"+rootPath;
System.out.println(msg);
return;
}
Files.walkFileTree(path, new FileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes arg1) throws IOException {
System.out.println(path.toString());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path arg0, IOException arg1) throws IOException {
System.out.println("向后遍历目录:" + arg0.toString());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path arg0, BasicFileAttributes arg1) throws IOException {
System.out.println("向前遍历目录:" + arg0.toString());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path arg0, IOException arg1) throws IOException {
return FileVisitResult.CONTINUE;
}
});
}
public static void main(String[] args) throws Exception {
String rootPath = "D:/temp";
printFileTree(rootPath);
}
}
测试结果图:
参考文献:
Java NIO Path接口和Files类配合操作文件