zoukankan      html  css  js  c++  java
  • Java NIO学习系列七:Path、Files、AsynchronousFileChannel

      相对于标准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:dataprojectsa-project..another-project
    path2 = d:dataprojectsanother-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的子类,提供了异步读取文件的能力。

  • 相关阅读:
    Leetcode 538. Convert BST to Greater Tree
    Leetcode 530. Minimum Absolute Difference in BST
    Leetcode 501. Find Mode in Binary Search Tree
    Leetcode 437. Path Sum III
    Leetcode 404. Sum of Left Leaves
    Leetcode 257. Binary Tree Paths
    Leetcode 235. Lowest Common Ancestor of a Binary Search Tree
    Leetcode 226. Invert Binary Tree
    Leetcode 112. Path Sum
    Leetcode 111. Minimum Depth of Binary Tree
  • 原文地址:https://www.cnblogs.com/volcano-liu/p/11220397.html
Copyright © 2011-2022 走看看