相對於標準Java IO中經過File來指向文件和目錄,Java NIO中提供了更豐富的類來支持對文件和目錄的操做,不只僅支持更多操做,還支持諸如異步讀寫等特性,本文咱們就來學習一些Java NIO提供的和文件相關的類:java
Java NIO Pathlinux
Java NIO Fileswindows
Java NIO AsynchronousFileChannel異步
總結async
Java Path是一個接口,位於java.nio.file包中,Java 7中引入到Java NIO中。ide
一個Java Path實現的實例對象表明文件系統中的一個路徑,指向文件和目錄,(標準Java IO中是經過File來指向文件和路徑的),以絕對路徑或者相對路徑的方式。post
java.nio.file.Path接口不少方面相似於java.io.File類,可是二者之間也是有細微的差異的。在大多數場景下是能夠用Path來代替File的。學習
能夠經過Paths類的靜態工廠方法get()來建立一個Path實例對象:spa
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"); } }
經過直接指定絕對路徑能夠建立使用絕對路徑方式指向文件的Path:線程
// windows系統 Path path = Paths.get("c:\\data\\myfile.txt"); // linux系統 Path path = Paths.get("/home/jakobjenkov/myfile.txt");
經過以下方式能夠建立使用相對路徑方式指向文件的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
Java NIO Files類(java.nio.file.Files)提供了一些方法用來操做文件,其是和上面提到的Path一塊兒配合使用的。
該方法能夠用來檢查Path指向的文件是否真實存在,直接看例子:
Path path = Paths.get("data/logging.properties"); boolean pathExists = Files.exists(path, new LinkOption[]{ LinkOption.NOFOLLOW_LINKS});
該方法會在硬盤上建立一個新的目錄(即文件夾):
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(); }
該方法會將文件從一個地方複製到另外一個地方:
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(); }
該方法可以移動文件,也能夠實現重命名的效果:
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(); }
該方法可以刪除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(); }
該方法刪除目錄時只能刪除空目錄,若是想刪除下面有文件的目錄則須要進行遞歸刪除,後面會介紹。
該方法可以遞歸地獲取目錄樹,該方法接收兩個參數,一個是指向目標目錄,另外一個是一個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個方法,分別在目錄轉換的四個不一樣階段調用:
這四個方法都會返回一個FileVisitResult枚舉對象,包含以下成員:
被調用的如上四個方法經過這些返回值來判斷是否要繼續遍歷目錄。
若是不想本身實現該接口,也可使用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來實現相似功能咱們還須要本身處理整個遍歷的過程。
java.nio.file.Files類還包含了不少別的有用方法,好比建立符號連接、文件大小、設置文件權限,這裏就不一一介紹了,有興趣的能夠參考Java官方文檔。
Java 7中引入了AsynchronousFileChannel,使得能夠異步地讀寫數據到文件。
經過其靜態方法能夠建立一個AsynchronousFileChannel。
Path path = Paths.get("data/test.xml");
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
第一個參數是一個指向要和AsynchronousFileChannel關聯的文件的Path實例。第二個參數表明要對文件指向的操做,這裏咱們指定StandardOpenOption.READ,意思是執行讀操做。
從AsynchronousFileChannel讀數據有兩種方式:
第一種方式是調用一個返回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參數的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()方法。
與讀相似,寫數據也支持兩種方式。
以下是一個寫數據的完整例子:
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來通知寫操做已經完成,示例以下:
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()方法。
本文總結了Java NIO中提供的對文件操做的相關類:Path、Files、AsynchronousFileChannel。
Path是一個接口,其實現實例能夠指代一個文件或目錄,做用與Java IO中的File相似。Path接口不少方面相似於java.io.File類,可是二者之間也是有細微的差異的,不過在大多數場景下是能夠用Path來代替File的。
Files是一個類,提供了不少方法用來操做文件,是和上面提到的Path一塊兒配合使用的,Files提供的對文件的操做功能要多於File。
AsynchronousFileChannel是Channel的子類,提供了異步讀取文件的能力。