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:\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的子類,提供了異步讀取文件的能力。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM