相对于标准 Java IO 中通过 File 来指向文件和目录, Java NIO 中提供了更丰富的类来支持对文件和目录的操作, 不仅仅支持更多操作, 还支持诸如异步读写等特性, 本文我们就来学习一些 Java NIO 提供的和文件相关的类:
- Java NIO Path
- Java NIO Files
- Java NIO AsynchronousFileChannel
总结
1. Java NIO Path
Java Path 是一个接口, 位于 java.nio.file 包中, Java 7 中引入到 Java NIO 中.
一个 Java Path 实现的实例对象代表文件系统中的一个路径, 指向文件和目录,(标准 Java IO 中是通过 File 来指向文件和路径的), 以绝对路径或者相对路径的方式.
java.nio.file.Path 接口很多方面类似于 java.io.File 类, 但是两者之间也是有细微的差别的. 在大多数场景下是可以用 Path 来代替 File 的.
1.1 创建 Path 实例对象
可以通过 Paths 类的静态工厂方法 get()来创建一个 Path 实例对象:
- import java.nio.file.Path;
- import java.nio.file.Paths;
- public class PathExample {
- public static void main(String[] args) {
- Path path = Paths.get("c:\\data\\myfile.txt");
- }
- }
- 1.2 Creating an Absolute Path
通过直接指定绝对路径可以创建使用绝对路径方式指向文件的 Path:
- // Windows 系统
- Path path = Paths.get("c:\\data\\myfile.txt");
- // Linux 系统
- Path path = Paths.get("/home/jakobjenkov/myfile.txt");
- 1.3 Creating a Relative Path
通过如下方式可以创建使用相对路径方式指向文件的 Path:
- Path projects = Paths.get("d:\\data", "projects");
- Path file = Paths.get("d:\\data", "projects\\a-project\\myfile.txt");
采用相对路径的方式时, 有两个符号可以用来表示路径:
.
..
"." 可以表示当前目录, 如下例子是打印当前目录(即应用程序的根目录):
- Path currentDir = Paths.get(".");
- System.out.println(currentDir.toAbsolutePath());
".." 表示父文件夹.
当路径中包含如上两种符号时, 可以通过调用 normalize()方法来将路径规范化:
- String originalPath = "d:\\data\\projects\\a-project\\..\\another-project";
- Path path1 = Paths.get(originalPath);
- System.out.println("path1 =" + path1);
- Path path2 = path1.normalize();
- System.out.println("path2 =" + path2);
输出结果如下:
- path1 = d:\data\projects\a-project\..\another-project
- path2 = d:\data\projects\another-project
- 2. Java NIO Files
Java NIO Files 类 (java.nio.file.Files) 提供了一些方法用来操作文件, 其是和上面提到的 Path 一起配合使用的.
2.1 Files.exists()
该方法可以用来检查 Path 指向的文件是否真实存在, 直接看例子:
- Path path = Paths.get("data/logging.properties");
- boolean pathExists = Files.exists(path, new LinkOption[]{
- LinkOption.NOFOLLOW_LINKS
- });
- 2.2 Files.createDirectory()
该方法会在硬盘上创建一个新的目录(即文件夹):
- Path path = Paths.get("data/subdir");
- try {
- Path newDir = Files.createDirectory(path);
- } catch(FileAlreadyExistsException e){
- // the directory already exists.
- } catch (IOException e) {
- //something else went wrong
- e.printStackTrace();
- }
- 2.3 Files.copy()
该方法会将文件从一个地方复制到另一个地方:
- Path sourcePath = Paths.get("data/logging.properties");
- Path destinationPath = Paths.get("data/logging-copy.properties");
- try {
- Files.copy(sourcePath, destinationPath);
- } catch(FileAlreadyExistsException e) {
- //destination file already exists
- } catch (IOException e) {
- //something else went wrong
- e.printStackTrace();
- }
如果目标文件已存在, 这里会抛出 java.nio.file.FileAlreadyExistsException 异常, 想要强制覆盖文件也是可以的:
- Path sourcePath = Paths.get("data/logging.properties");
- Path destinationPath = Paths.get("data/logging-copy.properties");
- try {
- Files.copy(sourcePath, destinationPath,
- StandardCopyOption.REPLACE_EXISTING);
- } catch(FileAlreadyExistsException e) {
- //destination file already exists
- } catch (IOException e) {
- //something else went wrong
- e.printStackTrace();
- }
- 2.4 Files.move()
该方法能够移动文件, 也可以实现重命名的效果:
- Path sourcePath = Paths.get("data/logging-copy.properties");
- Path destinationPath = Paths.get("data/subdir/logging-moved.properties");
- try {
- Files.move(sourcePath, destinationPath,
- StandardCopyOption.REPLACE_EXISTING);
- } catch (IOException e) {
- //moving file failed.
- e.printStackTrace();
- }
- 2.5 Files.delete()
该方法能够删除 Path 实例指向的文件或目录:
- Path path = Paths.get("data/subdir/logging-moved.properties");
- try {
- Files.delete(path);
- } catch (IOException e) {
- //deleting file failed
- e.printStackTrace();
- }
- Path path = Paths.get("data/subdir/logging-moved.properties");
- try {
- Files.delete(path);
- } catch (IOException e) {
- //deleting file failed
- e.printStackTrace();
- }
该方法删除目录时只能删除空目录, 如果想删除下面有文件的目录则需要进行递归删除, 后面会介绍.
2.6 Files.walkFileTree()
该方法能够递归地获取目录树, 该方法接收两个参数, 一个是指向目标目录, 另一个是一个 FileVisitor 类型对象:
- Files.walkFileTree(path, new FileVisitor<Path>() {
- @Override
- public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
- System.out.println("pre visit dir:" + dir);
- return FileVisitResult.CONTINUE;
- }
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
- System.out.println("visit file:" + file);
- return FileVisitResult.CONTINUE;
- }
- @Override
- public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
- System.out.println("visit file failed:" + file);
- return FileVisitResult.CONTINUE;
- }
- @Override
- public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
- System.out.println("post visit directory:" + dir);
- return FileVisitResult.CONTINUE;
- }
- });
FileVisitor 是一个接口, 你需要实现它, 接口的定义如下:
- public interface FileVisitor {
- public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException;
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException;
- public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException;
- public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
- }
该接口中包含 4 个方法, 分别在目录转换的四个不同阶段调用:
preVisitDirectory()方法在访问目录之前调用, 而 postVisitorDirectory()方法是在访问目录之后调用;
visitFile()方法会在访问每个文件 (访问目录是不会调用的) 时调用一次, 而 visitorFileFailed()会在访问文件失败时被调用, 比如没有访问权限或者别的问题.
这四个方法都会返回一个 FileVisitResult 枚举对象, 包含如下成员:
- CONTINUE
- TERMINATE
- SKIP_SIBLINGS
- SKIP_SUBTREE
被调用的如上四个方法通过这些返回值来判断是否要继续遍历目录.
CONTINUE, 意味着继续;
TERMINATE, 意味着终止;
SKIP_SIBLINGS, 意味着继续, 但是不再访问该文件或目录的兄弟;
SKIP_SUBTREE, 意味着继续, 但是不再访问该目录下的条目. 只有 preVisitDirectory()返回该值才有意义, 其余三个方法返回则会当做 CONTINUE 处理;
如果不想自己实现该接口, 也可以使用 SimpleFileVisitor, 这是一个默认实现, 如下是一个利用 SimpleFileVisitor 来实现文件查找, 删除的例子:
递归查找文件
- Path rootPath = Paths.get("data");
- String fileToFind = File.separator + "README.txt";
- try {
- Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
- String fileString = file.toAbsolutePath().toString();
- if(fileString.endsWith(fileToFind)){
- System.out.println("file found at path:" + file.toAbsolutePath());
- return FileVisitResult.TERMINATE;
- }
- return FileVisitResult.CONTINUE;
- }
- });
- } catch(IOException e){
- e.printStackTrace();
- }
递归删除目录
因为 delete()方法只能删除空目录, 对于非空目录则需要将其进行遍历以逐个删除其子目录或文件, 可以通过 walkFileTree()来实现, 在 visitFile()方法中删除子目录, 而在 postVisitDirectory()方法中删除该目录本身:
- Path rootPath = Paths.get("data/to-delete");
- try {
- Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
- System.out.println("delete file:" + file.toString());
- Files.delete(file);
- return FileVisitResult.CONTINUE;
- }
- @Override
- public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
- Files.delete(dir);
- System.out.println("delete dir:" + dir.toString());
- return FileVisitResult.CONTINUE;
- }
- });
- } catch(IOException e){
- e.printStackTrace();
- }
其实利用 walkFileTree()方法, 我们可以很轻松地指定自己的逻辑, 而无需考虑是如何遍历的, 如果要用标准 Java IO 提供的 File 来实现类似功能我们还需要自己处理整个遍历的过程.
2.7 其它有用方法
java.nio.file.Files 类还包含了很多别的有用方法, 比如创建符号链接, 文件大小, 设置文件权限, 这里就不一一介绍了, 有兴趣的可以参考 Java 官方文档.
3. Java NIO AsynchronousFileChannel
Java 7 中引入了 AsynchronousFileChannel, 使得可以异步地读写数据到文件.
3.1 Creating an AsynchronousFileChannel
通过其静态方法可以创建一个 AsynchronousFileChannel.
- Path path = Paths.get("data/test.xml");
- AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
第一个参数是一个指向要和 AsynchronousFileChannel 关联的文件的 Path 实例. 第二个参数代表要对文件指向的操作, 这里我们指定 StandardOpenOption.READ, 意思是执行读操作.
3.2 Reading Data
从 AsynchronousFileChannel 读数据有两种方式:
通过 Future 读数据
第一种方式是调用一个返回 Future 的 read()方法:
Future<Integer> operation = fileChannel.read(buffer, 0);
这个版本的 read()方法, 其第一个参数是一个 ByteBuffer, 数据从 channel 中读到 buffer 中; 第二个参数是要从文件中开始读取的字节位置.
该方法会马上返回, 即使读操作实际上还没有完成. 通过调用 Future 的 isDone()方法可以知道读操作是否完成了.
如下是一个更详细的例子:
- AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
- ByteBuffer buffer = ByteBuffer.allocate(1024);
- long position = 0;
- Future<Integer> operation = fileChannel.read(buffer, position);
- while(!operation.isDone());
- buffer.flip();
- byte[] data = new byte[buffer.limit()];
- buffer.get(data);
- System.out.println(new String(data));
- buffer.clear();
在这个例子中, 当调用了 AsynchronousFileChannel 的 read()方法之后, 进入循环直到 Future 对象的 isDone()返回 true. 当然这种方式并没有有效利用 CPU, 只是因为本例中需要等到读操作完成, 其实这个等待过程我们可以让线程做别的事情.
通过 CompletionHandler 读数据
第二种读数据的方式是调用其包含 CompletionHandler 参数的 read()方法:
- fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
- @Override
- public void completed(Integer result, ByteBuffer attachment) {
- System.out.println("result =" + result);
- attachment.flip();
- byte[] data = new byte[attachment.limit()];
- attachment.get(data);
- System.out.println(new String(data));
- attachment.clear();
- }
- @Override
- public void failed(Throwable exc, ByteBuffer attachment) {
- }
- });
当读操作完成之后会调用 ComplementHandler 的 completed()方法, 该方法的第一个入参是一个整型变量, 代表读了多少字节数据, 第二个入参是一个 ByteBuffer, 保存着已经读取的数据.
如果读失败了, 则会调用 ComplementHandler 的 fail()方法.
3.3 Writing Data
与读类似, 写数据也支持两种方式.
通过 Future 写
如下是一个写数据的完整例子:
- Path path = Paths.get("data/test-write.txt");
- AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
- ByteBuffer buffer = ByteBuffer.allocate(1024);
- long position = 0;
- buffer.put("test data".getBytes());
- buffer.flip();
- Future<Integer> operation = fileChannel.write(buffer, position);
- buffer.clear();
- while(!operation.isDone());
- System.out.println("Write done");
过程比较简单, 就不讲一遍了. 这个例子中有一个问题需要注意, 文件必须事先准备好, 如果不存在文件则会抛出 java.nio.file.NoSuchFileException 异常.
可以通过如下方式判断文件是否存在:
- if(!Files.exists(path)){
- Files.createFile(path);
- }
通过 CompletionHandler 写数据
可以借助 CompletionHandler 来通知写操作已经完成, 示例如下:
- Path path = Paths.get("data/test-write.txt");
- if(!Files.exists(path)){
- Files.createFile(path);
- }
- AsynchronousFileChannel fileChannel =
- AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
- ByteBuffer buffer = ByteBuffer.allocate(1024);
- long position = 0;
- buffer.put("test data".getBytes());
- buffer.flip();
- fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
- @Override
- public void completed(Integer result, ByteBuffer attachment) {
- System.out.println("bytes written:" + result);
- }
- @Override
- public void failed(Throwable exc, ByteBuffer attachment) {
- System.out.println("Write failed");
- exc.printStackTrace();
- }
- });
- System.out.println("异步执行哦");
如上是一个异步写入数据的例子, 为了演示效果, 我特意在 调用 write 方法之后打印了一行日志, 运行结果如下:
异步执行哦
bytes written: 9
说明调用 write 方法并没有阻塞, 而是继续往下执行, 所以先打印日志, 然后数据写好之后回调 completed()方法.
4. 总结
本文总结了 Java NIO 中提供的对文件操作的相关类: Path,Files,AsynchronousFileChannel.
Path 是一个接口, 其实现实例可以指代一个文件或目录, 作用与 Java IO 中的 File 类似. Path 接口很多方面类似于 java.io.File 类, 但是两者之间也是有细微的差别的, 不过在大多数场景下是可以用 Path 来代替 File 的.
Files 是一个类, 提供了很多方法用来操作文件, 是和上面提到的 Path 一起配合使用的, Files 提供的对文件的操作功能要多于 File.
AsynchronousFileChannel 是 Channel 的子类, 提供了异步读取文件的能力.
来源: https://www.cnblogs.com/volcano-liu/p/11220397.html