Java I/O系統學習系列三:I/O流的典型使用方式
儘管可以通過不同的方式組合IO流類,但我們可能也就只用到其中的幾種組合。下面的例子可以作為典型的IO用法的基本參考。在這些示例中,異常處理都被簡化為將異常傳遞給控制檯,但是這隻有在小型示例和工具中才適用。在程式碼中,你需要考慮更加複雜的錯誤處理方式。
同樣,本文會包括如下幾個方面:
緩衝輸入檔案
從記憶體輸入
格式化的記憶體輸入
基本的檔案輸出
儲存和恢復資料
讀寫隨機訪問檔案
實用工具
總結
1. 緩衝輸入檔案
如果想要開啟一個檔案用於字元輸入,可以使用以String或File物件作為檔名的FileReader。為了提高速度,我們可以對那個檔案進行緩衝,那麼我們需要將所產生的引用傳給一個BufferedReader構造器。通過使用其readLine()方法來逐行讀取檔案,當readLine()返回null時,就到了檔案末尾。
public class BufferedInputFile { public static String read(String fileName) throws Exception { BufferedReader br = new BufferedReader(new FileReader(fileName)); StringBuilder str = new StringBuilder(); String temp = null; while((temp = br.readLine()) != null) { str.append(temp + "\n"); } br.close(); return str.toString(); } public static void main(String[] args) { try { System.out.println(BufferedInputFile.read("pom.xml")); }catch(Exception e) { e.printStackTrace(); } } }
檔案的全部內容都累積在字串str中,最後記得呼叫close()來關閉流。
2. 從記憶體輸入
在下面的示例中,從上面的BufferedInputFile.read()讀入的String結果被用來建立一個StringReader。然後呼叫read()每次讀取一個字元,並把它列印到控制檯。
public class MemoryInput { public static void main(String[] args) { try { StringReader sr = new StringReader(BufferedInputFile.read("pom.xml")); int c; while((c = sr.read()) != -1) { System.out.print((char)c); } }catch(Exception e) { } } }
需要注意的是read()是以int形式返回下一個位元組,因此必須將型別強轉為char才能顯示正確結果。
3. 格式化的記憶體輸入
要讀取格式化資料,可以使用DataInputStream,它是一個面向位元組的I/O類,我們可以用InputStream以位元組的形式讀取任何資料。
public class FormattedMemoryInput { public static void main(String[] args) { try { DataInputStream di = new DataInputStream(new ByteArrayInputStream(BufferedInputFile.read("pom.xml").getBytes())); while(di.available() != 0) { System.out.print((char)di.readByte()); } }catch (Exception e) { e.printStackTrace(); } } }
這裡需要注意必須為ByteArrayInputStream的建構函式提供位元組陣列,而ByteArrayInputStream傳遞給DataInputStream之後進行了一次“裝飾”,可以進行格式化輸入(比如直接讀取int、double等型別),這裡我們只是通過readByte讀取單個位元組,呼叫該方法時任何位元組的值都是合法的結果,因此返回值是不能用來檢測輸入是否結束,這裡我們使用available()方法檢視還有多少可供存取的字元來判斷是否結束。
4. 基本的檔案輸出
FileWriter物件可以向檔案寫入資料,通常會用BufferedWriter將其包裝起來用以緩衝輸出以提高效能。在本例中為了提供格式化機制,將其裝飾成PrintWriter:
public class BasicFileOutput { static String file = "BasicFileOutput.out"; public static void main(String[] args) { try { BufferedReader in = new BufferedReader(new StringReader(BufferedInputFile.read("pom.xml"))); PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file))); String temp; int count = 0; while((temp = in.readLine()) != null) { out.println(count++ + temp); } in.close(); out.close(); System.out.println(BufferedInputFile.read(file)); }catch(Exception e) { e.printStackTrace(); } } }
這裡在讀取BasicFileOutput.out的內容之前,先呼叫了out的close()將其關閉,一方面是因為流用完之後需要及時關閉以節省資源,另一方面這裡用到了緩衝區,如果不為所有的輸出檔案呼叫close(),緩衝區的內容可能不會重新整理清空,這樣可能導致資訊不完整。
另外Java SE5在PrintWriter中添加了一個輔助構造器,可以很方便根據檔名直接構造一個PrintWriter而不用執行一系列的裝飾工作:
PrintWriter out = new PrintWriter(file);
5. 儲存和恢復資料
PrintWriter可以對資料進行格式化,以便閱讀。但是為了輸出可供另一個“流”恢復的資料,我們需要用DataOutputStream寫入資料,並用DataInputStream恢復資料。當然,這些流可以是任何形式,在下面的例子中使用的是一個檔案。
public class StoringAndRecoveringData { public static void main(String[] args) { try { DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("data.txt"))); out.writeDouble(3.14159); out.writeUTF("That was pi"); out.writeDouble(1.41413); out.writeUTF("Square root of 2"); out.close(); DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("data.txt"))); System.out.println(in.readDouble()); System.out.println(in.readUTF()); System.out.println(in.readDouble()); System.out.println(in.readUTF()); in.close(); }catch(Exception e) { e.printStackTrace(); } } }
使用DataOutputStream寫入資料,Java可以保證我們可以使用DataInputStream準確地讀取資料--無論讀和寫資料的平臺多麼不同。當我們使用DataOutputStream時,寫字串並且讓DataInputStream能夠恢復它的唯一可靠的做法就是使用UTF-8編碼,在這個例子中是靠writeUTF()和readUTF()來實現的。
writeDouble()和readDouble()方法能夠寫入和恢復double型別的資料。對於其他型別的資料,也有類似的方法用於讀寫。但是為了保證所有的讀寫方法都能夠正常工作,我們必須知道流中資料項所在的確切位置,因為極有可能將儲存的double資料作為一個簡單的位元組序列、char或其他型別讀入。
6. 讀寫隨機訪問檔案
使用RandomAccessFile,類似於組合使用了DataInputStream和DataOutputStream,可以同時對一個檔案執行讀寫操作,同時可以利用seek()在檔案中到處移動,非常方便,關於RandomAccessFile的詳細用法,前面有專門寫過<<Java I/O系統:File和RandomAccessFile>>。
但是在使用RandomAccessFile時,你需要知道檔案的排版,這樣才能正確地操作它,RandomAccessFile擁有讀取基本型別和UTF-8字串的各種具體方法。
public class UsingRandomAccessFile{ static String file = "rtest.dat"; static void display() throws IOException{ RandomAccessFile rf = new RandomAccessFile(file,"r"); for(int i = 0; i < 7; i++){ System.out.println("Value " + i + ": " + rf.readDouble()); } System.out.println(rf.readUTF()); rf.close(); } public static void main(String[] args) throws IOException{ RandomAccessFile rf = new RandomAccessFile(file,"rw"); for(int i = 0; i < 7; i++){ rf.writeDouble(i*1.414); } rf.writeUTF("The end of the file"); rf.close(); display(); rf = new RandomAccessFile(file,"rw"); rf.seek(5*8); rf.writeDouble(47.0001); rf.close(); display(); } }
我們通過writeDouble()方法往檔案中寫入Double型別資料並通過readDouble()方法來讀取,這就是我們需要直到排版的原因,如果讀取的不是Double型別的資料有可能出現不是我們想要的結果。
7. 實用工具
到這裡我們學習了多種I/O流的典型用法,比如緩衝輸入檔案、從記憶體輸入、基本的檔案輸出、儲存和恢復資料、隨機讀寫檔案,這些都是Java I/O流比較典型的用法。這裡我們發現讀取檔案、修改、在寫出是一個很常見的程式化的任務,但是Java I/O類庫的設計有一個問題,就是我們需要編寫很多程式碼來實現這些操作,要記住如何開啟檔案是一件優點困難的事情。因此,下面是收集的一些幫助類,可以很容易為我們完成這些基本任務,記錄在這裡,方便以後檢視。
這裡收集了兩個工具:
- 一個是TextFile,幫助我們讀取和寫入檔案;
- 另一個是BinaryFile,幫助我們簡化二進位制檔案的讀取。
7.1 讀取檔案
TextFile類包含的static方法可以像簡單字串那樣讀寫文字檔案,並且我們可以建立一個TextFile物件,它用一個ArrayList來儲存檔案的若干行,好處是在我們操縱檔案內容時可以使用ArrayList的所有功能。
public class TextFile extends ArrayList<String>{ // 將檔案讀取到一行字串中 public static String read(String fileName){ StringBuilder sb = new StringBuilder(); try{ BufferedReader in = new BufferedReader(new FileReader(new File(fileName).getAbsoluteFile())); try{ String s;; while((s = in.readLine()) != null){ sb.append(s).append("\n"); } }finally{ in.close(); } }catch (IOException e){ throw new RuntimeException(e); } return sb.toString(); } // 單次呼叫將一個字串寫入一個檔案 public static void write(String fileName,String text){ try{ PrintWriter out = new PrintWriter(new File(fileName).getAbsoluteFile()); try{ out.print(text); }finally{ out.close(); } }catch(IOException e){ throw new RuntimeException(e); } } // 讀取檔案,並通過正則表示式將其分離,儲存在List中 public TextFile(String fileName,String splitter){ super(Arrays.asList(read(fileName).split(splitter))); // 因為split()方法有時會在返回的陣列第一個位置產生一個空字串 if(get(0).equals("")) remove(0); } // 常規的分行讀取 public TextFile(String fileName){ this(fileName,"\n"); } // 將該TextFile中的內容分行寫入指定檔案中 public void write(String fileName){ try{ PrintWriter out = new PrintWriter(new File(fileName).getAbsoluteFile()); try{ for(String item : this){ out.println(item); } }finally{ out.close(); } }catch(IOException e){ throw new RuntimeException(e); } } // 簡單驗證一下 public static void main(String[] args){ String file = read("TextFile.java"); write("test.txt",file); TextFile text = new TextFile("test.txt"); text.write("test2.txt"); TreeSet<String> words = new TreeSet<String>(new TextFile("TextFile.java","\\W+")); System.out.println(words.headSet("a")); } }
這裡利用靜態的read()方法將檔案讀取到一個字串中,再用靜態的write()方法將其寫入到檔案中。然後將新寫入的檔案作為構造引數構造一個TestFile物件,利用其List的特性,將其內容寫入檔案test2中。這個類的作用是幫我們讀取檔案,可以通過靜態的read方法讀取到一個字串中,也可以通過構造器讀取檔案到一個TextFile物件中。
7.2 讀取二進位制檔案
public class BinaryFile{ public static byte[] read(File bFile)throws IOException{ BufferedInputStream bf = new BufferedInputStream(new FileInputStream()); try{ byte[] data = new byte[bf.available()]; br.read(data); return data; }finally{ bf.close(); } } public static byte[] read(String bFile)throws IOException{ return read(new File(bFile).getAbsoluteFile()); } }
8. 總結
本文沒有總結什麼新的知識點,只是總結了一些Java I/O的常見用法比如緩衝輸入檔案、從記憶體輸入、基本的檔案輸出、儲存和恢復資料、隨機讀寫檔案等,並且蒐集了兩個工具類用來幫助我們讀寫檔案讀取二進位制檔案,以提高些程式碼的效率。
&n