Java 一直到 JDK 1.3 為止,都是使用 java.io 下的類別進行 I/O 的處理,對這有興趣的鄉民,可以參考「Java I/O」,那是我 13 年前整理的 ... XD。JDK 1.4 之後提供了 NIO,到了 JDK 1.7 又加上些新的功能,稱為 NIO.2,這些類別都被放在 java.nio 下,一般來說,使用 java.nio 類別操作檔案系統,會比使用 java.io 效率來的高且方便。
在進入主題前,各位不妨先看一下我之前整理的「Working with the Path Class」及「Metadata File Attributes」,這算是進入主題的前菜,這篇開始要說明怎麼開檔、讀檔、寫檔。
- 開檔
不管是要讀檔或寫檔,第一個步驟總得要開檔,java.nio 提供了以下的開檔方式:
- READ: 要讀取檔案的內容
- WRITE: 要寫資料到檔案
- CREATE: 建立一個新檔,如果檔案已存在,將它刪除,重新建立新檔。
- CREATE_NEW: 建立一個新檔,當檔案已存在,拋出 exception。
- APPEND: 附加內容到已存在的檔案。
- DELETE_ON_CLOSE: 這個選項是用在暫存檔上,當檔案關閉時,將這個檔案刪除。
- TRUNCATE_EXISTING: 將檔案內容清除,然後再開始寫入。
- SPARSE:
- SYNC: 保持檔案內容和 metadata 不變。
- DSYNC: 保持檔案內容不變。
底下是一個最典型的範例:
1 package idv.steven.nio2.filedir; 2 3 import java.io.IOException; 4 import java.nio.channels.ReadableByteChannel; 5 import java.nio.file.Files; 6 import java.nio.file.Path; 7 import java.nio.file.Paths; 8 import java.nio.file.StandardOpenOption; 9 10 public class NIO2File { 11 public static void main(String[] args) throws IOException { 12 Path path = Paths.get("C:/Java/poi-3.11/NOTICE"); 13 ReadableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ); 14 //... 15 } 16 }
在 java.io 中,I/O 都是在串流 (stream) 中操作,到了 java.nio 都改為渠道 (channel),上面的程式是開啟一個讀取的檔案,所有開檔模式都定義在 StandOpenOption 這個自定型別中。如果要開啟一個寫入的檔案,第 13 行可能就改為如下:
WritableByteChannel channel = Files.newByteChannel(path, new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.WRITE});
寫檔時不能只有 WRITE 這個參數,還要再加入 CREATE、CREATE_NEW 等參數,所有參數放在一個 OpenOption 陣列裡。
- 渠道 (Channel)
java.nio 定義了那些 channel ? 如下類別圖所示:
上圖只列出常用的介面及其 method,要了解詳細的類別繼承關係,請看 JDK Doc。操作檔案時,最常用的就是 ReadableByteChannel、WritableByteChannel 及 SeekableByteChannel,前兩者顧名思義,是分別用在讀檔及寫檔,第三個是同時可用在讀與寫的渠道,且可以在讀寫過程裡移動檔案指標。至於 NetworkChannel 和 MulticastChannel 是用在網路的 TCP 和 UDP 傳輸上,會另外說明。
在 java.nio 中定義了渠道取代 java.io 中的 stream,同時渠道操作的物件也不是 byte[]、char[]… 改成 java.nio 自行定義的 ByteBuffer、CharBuffer 等類別,最常用的是 ByteBuffer,關於 ByteBuffer 的說明,請看「ByteBuffer 指標說明」。
那麼,渠道和串流 (stream) 倒底有什麼不同? 整理如下:
- 串流一般來說都是 one-way,也就是只能讀或只能寫,渠道支援同時讀寫。
- 渠道可以非同步的讀寫。
- 渠道如果讀資料,一定是讀入 Buffer (ByteBuffer、CharBuffer...),寫資料的話,也一定要先把資料放入 Buffer 再透過渠道寫出。
- 讀檔
1 package idv.steven.nio2.filedir; 2 3 import java.io.IOException; 4 import java.nio.ByteBuffer; 5 import java.nio.channels.ReadableByteChannel; 6 import java.nio.file.Files; 7 import java.nio.file.Path; 8 import java.nio.file.Paths; 9 import java.nio.file.StandardOpenOption; 10 11 public class NIO2File { 12 public static void main(String[] args) throws IOException { 13 Path path = Paths.get("C:/Java/poi-3.11/NOTICE"); 14 ReadableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ); 15 16 ByteBuffer buffer = ByteBuffer.allocate(1024); 17 18 while (channel.read(buffer) > 0) { 19 System.out.println(new String(buffer.array())); 20 buffer.flip(); 21 } 22 23 channel.close(); 24 } 25 }
上面的程式,於 18 行每次讀取 1024 個 byte,直到檔尾為止,讀取後放入 buffer 中,在 19 行將讀取的內容輸出,讀完後當然要記得關檔 (23行)。
- 寫檔
1 package idv.steven.nio2.filedir; 2 3 import java.io.IOException; 4 import java.nio.ByteBuffer; 5 import java.nio.channels.ReadableByteChannel; 6 import java.nio.channels.WritableByteChannel; 7 import java.nio.file.Files; 8 import java.nio.file.OpenOption; 9 import java.nio.file.Path; 10 import java.nio.file.Paths; 11 import java.nio.file.StandardOpenOption; 12 13 public class NIO2File { 14 public static void main(String[] args) throws IOException { 15 Path path = Paths.get("C:/Java/poi-3.11/NOTICE"); 16 ReadableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ); 17 18 Path pathTo = Paths.get("C:/Java/poi-3.11/NOTICE.txt"); 19 WritableByteChannel channelTo = Files.newByteChannel(pathTo, 20 new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.WRITE}); 21 22 ByteBuffer buffer = ByteBuffer.allocate(1024); 23 while ((channel.read(buffer)) > 0) { 24 buffer.flip(); 25 channelTo.write(buffer); 26 buffer.flip(); 27 } 28 29 channel.close(); 30 channelTo.close(); 31 } 32 }
這個程式只是將上一個程式擴充,將由 NOTICE 讀出的內容,寫入 NOTICE.txt 檔裡,在 19~20 行開啟一個寫入的檔案,於 24~26 行將讀到的內容寫入指定的檔案。
- 小型檔案的讀寫
上面的讀寫檔案的方法可同時適用在大檔案、小檔案,如果我們預先就知道,要讀寫的檔案很小,有更簡單的方式,如下:
1 package idv.steven.nio2.filedir; 2 3 import java.io.IOException; 4 import java.nio.file.Files; 5 import java.nio.file.OpenOption; 6 import java.nio.file.Path; 7 import java.nio.file.Paths; 8 import java.nio.file.StandardOpenOption; 9 10 public class SmallFile { 11 12 public static void main(String[] args) throws IOException { 13 Path path = Paths.get("C:/Java/poi-3.11/NOTICE"); 14 Path pathTo = Paths.get("C:/Java/poi-3.11/NOTICE.txt"); 15 16 byte[] smallArray = Files.readAllBytes(path); 17 Files.write(pathTo, smallArray, 18 new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.WRITE}); 19 } 20 }
如上,在 16 行的地方,一次將所有內容讀入一個 byte array,在 17 行的地方,再一次將 byte array 的內容寫入檔案裡。
- 讀寫文字檔
到目前為止,我們讀寫檔案的方式,都可適用在各種類型的檔案,如上面,我們處理的是文字檔,事實上,圖檔、執行檔 … 也都不會有問題。如果我們要處理的就是文字檔呢? 文字檔通常就是每一行後面會有列尾符號,所以讀取時可以一次讀一行,寫出時也應該一次寫一行,將上面的程式改寫如下:
1 package idv.steven.nio2.filedir; 2 3 import java.io.IOException; 4 import java.nio.charset.Charset; 5 import java.nio.file.Files; 6 import java.nio.file.OpenOption; 7 import java.nio.file.Path; 8 import java.nio.file.Paths; 9 import java.nio.file.StandardOpenOption; 10 import java.util.List; 11 12 public class RWText { 13 14 public static void main(String[] args) throws IOException { 15 Path path = Paths.get("D:/novel.txt"); 16 Path pathTo = Paths.get("D:/novel_1.txt"); 17 18 Charset charset = Charset.forName("MS950"); 19 List<String> lines = Files.readAllLines(path, charset); 20 Files.write(pathTo, lines, charset, 21 new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.WRITE}); 22 } 23 }
文字檔一般會有編碼問題,這裡用來測試的文字檔 novel.txt 是繁體漢字的一個文字檔,採用 MS950 編碼,所以 19 行讀取的時候要指定檔案的編碼,20 行寫出時也一樣,要指出是什麼編碼。讀取或寫入時不指定編碼,預設就是 UTF-8。
沒有留言:
張貼留言