相對於標準Java IO中通過File來指向檔案和目錄,Java NIO中提供了更豐富的類來支援對檔案和目錄的操作,不僅僅支援更多操作,還支援諸如非同步讀寫等特性,本文我們就來學習一些Java NIO提供的和檔案相關的類:
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的子類,提供了非同步讀取檔案的能力。