Java:前程似錦的 NIO 2.0
Java 之所以能夠霸佔程式語言的榜首,其強大、豐富的類庫功不可沒,幾乎所有的程式設計問題都能在其中找到解決方案。但在早期的版本當中,輸入輸出(I/O)流並不那麼令開發者感到愉快:
1)JDK 1.4 之前的 I/O 沒有緩衝區的概念、不支援正則表示式、支援的字符集編碼有限等等;
2)JDK 1.4 的時候引入了非阻塞 I/O,也就是 NIO 1.0,但遍歷目錄很困難,不支援檔案系統的非阻塞操作等等。
為了突破這些限制,JDK 1.7 的時候引入了新的 NIO,也就是本篇文章的主角——NIO 2.0。
01、基石:Path
Path 既可以表示一個目錄,也可以表示一個檔案,就像 File 那樣——當然了,Path 就是用來取代 File 的。
1)可以通過 Paths.get()
建立一個 Path 物件,此時 Path 並沒有真正在物理磁碟上建立;引數既可以是一個檔名,也可以是一個目錄名;絕對路徑或者相對路徑均可。
2)可以通過 Files.notExists()
確認 Path(目錄或者檔案) 是否已經存在。
3)可以通過 Files.createDirectory()
建立目錄,此時目錄已經在物理磁碟上建立成功,可通過資源管理器檢視到。
4)可以通過 Files.createFile()
建立檔案,此時檔案已經在物理磁碟上建立成功,可通過資源管理器檢視到。
5)可以通過 toAbsolutePath()
檢視 Path 的絕對路徑。
6)可以通過 resolve()
將 Path 連線起來,引數可以是一個新的 Path 物件,也可以是對應的字串。
具體的程式碼如下:
public class Wanger {
public static void main(String[] args) {
// 相對路徑
Path dir = Paths.get("chenmo");
// 輸出 dir 的絕對路徑
System.out.println(dir.toAbsolutePath()); // 輸出:D:\program\java.git\java_demo\chenmo
if (Files.notExists(dir)) {
try {
// 如果目錄不存在,則建立目錄
Files.createDirectory(dir);
} catch (IOException e1) {
e1.printStackTrace();
}
}
// 這時候 chenmo.txt 檔案並未建立
// 通過 resolve 方法把 dir 和 chenmo.txt 連結起來
Path file = dir.resolve("chenmo.txt");
// 輸出 file 的絕對路徑
System.out.println(file.toAbsolutePath()); // 輸出:D:\program\java.git\java_demo\chenmo\chenmo.txt
if (Files.notExists(file)) {
try {
// 如果檔案不存在,則建立檔案
Files.createFile(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
如果要將 File 轉換為 Path,可以通過 File 類的 toPath()
方法完成。程式碼示例如下:
File file = new File("沉默王二.txt");
Path path = file.toPath();
如果要將 Path 轉換為 File,可以通過 Path 類的 toFile()
方法完成。程式碼示例如下:
Path path = Paths.get("沉默王二.txt");
File file = path.toFile();
02、處理目錄
NIO 2.0 新增的 java.nio.file.DirectoryStream<T>
介面可以非常方便地查詢目錄中的(符合某種規則的)檔案,比如說我們要查詢 chenmo 目錄下的 txt 字尾的檔案,程式碼示例如下:
// 相對路徑
Path dir = Paths.get("chenmo");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.txt")) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
} catch (IOException e) {
e.printStackTrace();
}
1)Files.newDirectoryStream(Path dir, String glob)
會返回一個過濾後的 DirectoryStream( 目錄流,),第一個引數為目錄,第二個引數為 glob 表示式,比如 *.txt
表示所有 txt 字尾的檔案。
2)由於 DirectoryStream 繼承了 Closeable 介面,所以它可以配合 try-with-resources 語法寫出更安全的程式碼,目錄流會自動呼叫 close 方法關閉流,釋放與流相關的資源,不需要再通過 finally 進行主動關閉。
3)DirectoryStream 被稱為目錄流,允許方便地使用 for-each 結構來遍歷目錄。
03、處理目錄樹
目錄樹意味著一個目錄裡既有檔案也有子目錄,也可能都沒有,也可能有其一。NIO 2.0 可以很方便地遍歷一顆目錄樹,並操作符合條件的檔案;這其中關鍵的一個方法就是 Files 類的 walkFileTree,其定義如下:
public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
throws IOException
{
return walkFileTree(start,
EnumSet.noneOf(FileVisitOption.class),
Integer.MAX_VALUE,
visitor);
}
第二個引數 FileVisitor 被稱為檔案訪問器介面,它實現起來非常複雜,要實現 5 個方法呢,但幸好 JDK 的設計者提供了一個預設的實現類 SimpleFileVisitor,如果我們只想從目錄樹中找到 txt 字尾的檔案,可以這樣做:
// 相對路徑
Path dir = Paths.get("chenmo");
try {
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".txt")) {
System.out.println(file.getFileName());
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
e.printStackTrace();
}
通過建立匿名內部類來重寫 SimpleFileVisitor 的 visitFile 方法,如果字尾名為 txt 就打印出來。
04、檔案的刪除、複製、移動
建立一個檔案非常的簡單,之前我們已經體驗過了,那麼刪除一個檔案也同樣的簡單,程式碼示例如下:
Files.delete(file);
Files.deleteIfExists(file);
使用 Files.delete()
刪除檔案之前最好使用 Files.exists()
判斷檔案是否存在,否則會丟擲 NoSuchFileException;Files.deleteIfExists()
則不用。
複製檔案也不復雜,程式碼示例如下:
Path source = Paths.get("沉默王二.txt");
Path target = Paths.get("沉默王二1.txt");
Files.copy(source, target);
移動檔案和複製檔案非常相似,程式碼示例如下:
Path source = Paths.get("沉默王二.txt");
Path target = Paths.get("沉默王二1.txt");
Files.move(source, target);
05、快速地讀寫檔案
NIO 2.0 提供了帶有緩衝區的讀寫輔助方法,使用起來也非常的簡單。可以通過 Files.newBufferedWriter()
獲取一個檔案緩衝輸入流,並通過 write()
方法寫入資料;然後通過 Files.newBufferedReader()
獲取一個檔案緩衝輸出流,通過 readLine()
方法讀出資料。程式碼示例如下。
Path file = Paths.get("沉默王二.txt");
try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
writer.write("一個有趣的程式設計師");
} catch (Exception e) {
e.printStackTrace();
}
try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
06、重要:非同步 I/O 操作
實話實說吧,上面提到的那些都算是 NIO 2.0 的甜點,而非同步 I/O 操作(也稱 AIO)才算是真正重要的內容。非同步 I/O 操作可以充分利用多核 CPU 的特點,不需要再像以前那樣啟動一個執行緒來對 I/O 進行處理,免得阻塞了主執行緒的其他操作。
非同步 I/O 操作的核心概念是發起非阻塞方式的 I/O 操作,當 I/O 操作完成時通知。可以分為兩種形式:Future 和 Callback。如果我們希望主執行緒發起 I/O 操作並輪循等待結果時,一般使用 Future 的形式;而 Callback 的基本思想是主執行緒派出一個偵查員(CompletionHandler)到獨立的執行緒中執行 I/O 操作,操作完成後,會觸發偵查員的 completed 或者 failed 方法。
1)Future
先來看一個示例,程式碼如下:
public static void main(String[] args) throws IOException, InterruptedException, ExecutionException {
Path file = Paths.get("沉默王二.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);
Future<Integer> result = channel.read(ByteBuffer.allocate(100_000), 0);
while (!result.isDone()) {
System.out.println("主執行緒繼續做事情");
}
Integer bytesRead = result.get();
System.out.println(bytesRead);
}
1)通過 AsynchronousFileChannel.open()
開啟一個非同步檔案通道 channel。
2)用 Future 來儲存從通道中讀取的結果。
3)通過 isDone()
輪循判斷非同步 I/O 操作是否完成,如果沒有完成的話,主執行緒可以繼續做自己的事情。
2)Callback
先來看一個示例,程式碼如下:
public static void main(String[] args) throws IOException, InterruptedException, ExecutionException {
Path file = Paths.get("沉默王二.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);
channel.read(ByteBuffer.allocate(100_000), 0, null, new CompletionHandler<Integer, ByteBuffer>() {
public void completed(Integer result, ByteBuffer attachment) {
System.out.println(result);
}
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println(exc.getMessage());
}
});
System.out.println("主執行緒繼續做事情");
}
1)通過 AsynchronousFileChannel.open()
開啟一個非同步檔案通道 channel。
2)在 read 方法中使用匿名內部類的形式啟用 CompletionHandler,然後實現 CompletionHandler 的兩個監聽方法,completed 的時候列印結果,failed 的時候列印異常資訊。
不管是 Future 形式還是 Callback 形式,總之非同步 I/O 是一個強大的特性,可以保證在處理大檔案時效能不受到顯著的影