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的子类,提供了异步读取文件的能力。

  • 相关阅读:
    从一个整数数组中取出最大的整数,最小整数,总和,平均值
    9、数组知识点小结
    结构类型小结
    枚举类型小结
    asp.net MVC 笔记
    Android自动化测试之Shell脚本一——模拟触屏事件
    Android性能优化案例研究
    ViewHolder模式的简洁写法
    genymotion ddms查看data等文件目录
    Android事件传递机制
  • 原文地址:https://www.cnblogs.com/volcano-liu/p/11220397.html
Copyright © 2011-2022 走看看