原文同步至:http://www.waylau.com/essential-java-io-streams/html
本文詳細介紹了 Java I/O 流的基礎用法和原理。java
字節流處理原始的二進制數據 I/O。輸入輸出的是8位字節,相關的類爲 InputStream 和 OutputStream.git
字節流的類有許多。爲了演示字節流的工做,咱們將重點放在文件 I/O字節流 FileInputStream 和 FileOutputStream 上。其餘種類的字節流用法相似,主要區別在於它們構造的方式,你們能夠觸類旁通。程序員
下面一例子 CopyBytes, 從 xanadu.txt 文件複製到 outagain.txt,每次只複製一個字節:github
public class CopyBytes { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { FileInputStream in = null; FileOutputStream out = null; try { in = new FileInputStream("resources/xanadu.txt"); out = new FileOutputStream("resources/outagain.txt"); int c; while ((c = in.read()) != -1) { out.write(c); } } finally { if (in != null) { in.close(); } if (out != null) { out.close(); } } } }
CopyBytes 花費其大部分時間在簡單的循環裏面,從輸入流每次讀取一個字節到輸出流,如圖所示:正則表達式
再也不須要一個流記得要關閉它,這點很重要。因此,CopyBytes 使用 finally 塊來保證即便發生錯誤兩個流仍是能被關閉。這種作法有助於避免嚴重的資源泄漏。編程
一個可能的錯誤是,CopyBytes 沒法打開一個或兩個文件。當發生這種狀況,對應解決方案是判斷該文件的流是不是其初始 null 值。這就是爲何 CopyBytes 能夠確保每一個流變量在調用前都包含了一個對象的引用。api
CopyBytes 彷佛是一個正常的程序,但它實際上表明瞭一種低級別的 I/O,你應該避免。由於 xanadu.txt 包含字符數據時,最好的方法是使用字符流,下文會有討論。字節流應只用於最原始的 I/O。全部其餘流類型是創建在字節流之上的。數組
字符流處理字符數據的 I/O,自動處理與本地字符集轉化。緩存
Java 平臺存儲字符值使用 Unicode 約定。字符流 I/O 會自動將這個內部格式與本地字符集進行轉換。在西方的語言環境中,本地字符集一般是 ASCII 的8位超集。
對於大多數應用,字符流的 I/O 不會比 字節流 I/O操做複雜。輸入和輸出流的類與本地字符集進行自動轉換。使用字符的程序來代替字節流能夠自動適應本地字符集,並能夠準備國際化,而這徹底不須要程序員額外的工做。
若是國際化不是一個優先事項,你能夠簡單地使用字符流類,而沒必要太注意字符集問題。之後,若是國際化成爲當務之急,你的程序能夠方便適應這種需求的擴展。見國際化獲取更多信息。
字符流類描述在 Reader 和 Writer。而對應文件 I/O ,在 FileReader 和 FileWriter,下面是一個 CopyCharacters 例子:
public class CopyCharacters { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { FileReader inputStream = null; FileWriter outputStream = null; try { inputStream = new FileReader("resources/xanadu.txt"); outputStream = new FileWriter("resources/characteroutput.txt"); int c; while ((c = inputStream.read()) != -1) { outputStream.write(c); } } finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } } }
CopyCharacters 與 CopyBytes 是很是類似的。最重要的區別在於 CopyCharacters 使用的 FileReader 和 FileWriter 用於輸入輸出,而 CopyBytes 使用 FileInputStream 和FileOutputStream 中的。請注意,這兩個CopyBytes和CopyCharacters使用int變量來讀取和寫入;在 CopyCharacters,int 變量保存在其最後的16位字符值;在 CopyBytes,int 變量保存在其最後的8位字節的值。
字符流每每是對字節流的「包裝」。字符流使用字節流來執行物理I/O,同時字符流處理字符和字節之間的轉換。例如,FileReader 使用 FileInputStream,而 FileWriter使用的是 FileOutputStream。
有兩種通用的字節到字符的「橋樑」流:InputStreamReader 和 OutputStreamWriter。當沒有預包裝的字符流類時,使用它們來建立字符流。在 socket 章節中將展現該用法。
字符 I/O 一般發生在較大的單位不是單個字符。一個經常使用的單位是行:用行結束符結尾。行結束符能夠是回車/換行序列(「\r\n
」),一個回車(「\r
」),或一個換行符(「\n
」)。支持全部可能的行結束符,程序能夠讀取任何普遍使用的操做系統建立的文本文件。
修改 CopyCharacters 來演示如使用面向行的 I/O。要作到這一點,咱們必須使用兩個類,BufferedReader 和 PrintWriter 的。咱們會在緩衝 I/O 和Formatting 章節更加深刻地研究這些類。
該 CopyLines 示例調用 BufferedReader.readLine 和 PrintWriter.println 同時作一行的輸入和輸出。
public class CopyLines { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { BufferedReader inputStream = null; PrintWriter outputStream = null; try { inputStream = new BufferedReader(new FileReader("resources/xanadu.txt")); outputStream = new PrintWriter(new FileWriter("resources/characteroutput.txt")); String l; while ((l = inputStream.readLine()) != null) { outputStream.println(l); } } finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } } }
調用 readLine 按行返回文本行。CopyLines 使用 println 輸出帶有當前操做系統的行終止符的每一行。這可能與輸入文件中不是使用相同的行終止符。
除字符和行以外,有許多方法來構造文本的輸入和輸出。欲瞭解更多信息,請參閱 Scanning 和 Formatting。
緩衝流經過減小調用本地 API 的次數來優化的輸入和輸出。
目前爲止,大多數時候咱們到看到使用非緩衝 I/O 的例子。這意味着每次讀或寫請求是由基礎 OS 直接處理。這可使一個程序效率低得多,由於每一個這樣的請求一般引起磁盤訪問,網絡活動,或一些其它的操做,而這些是相對昂貴的。
爲了減小這種開銷,因此 Java 平臺實現緩衝 I/O 流。緩衝輸入流從被稱爲緩衝區(buffer)的存儲器區域讀出數據;僅當緩衝區是空時,本地輸入 API 才被調用。一樣,緩衝輸出流,將數據寫入到緩存區,只有當緩衝區已滿才調用本機輸出 API。
程序能夠轉換的非緩衝流爲緩衝流,這裏用非緩衝流對象傳遞給緩衝流類的構造器。
inputStream = new BufferedReader(new FileReader("xanadu.txt")); outputStream = new BufferedWriter(new FileWriter("characteroutput.txt"));
用於包裝非緩存流的緩衝流類有4個:BufferedInputStream 和 BufferedOutputStream 用於建立字節緩衝字節流, BufferedReader 和 BufferedWriter 用於建立字符緩衝字節流。
刷新緩衝區是指在某個緩衝的關鍵點就能夠將緩衝輸出,而沒必要等待它填滿。
一些緩衝輸出類經過一個可選的構造函數參數支持 autoflush(自動刷新)。當自動刷新開啓,某些關鍵事件會致使緩衝區被刷新。例如,自動刷新 PrintWriter 對象在每次調用 println 或者 format 時刷新緩衝區。查看 Formatting 瞭解更多關於這些的方法。
若是要手動刷新流,請調用其 flush 方法。flush 方法能夠用於任何輸出流,但對非緩衝流是沒有效果的。
掃描和格式化容許程序讀取和寫入格式化的文本。
I/O 編程一般涉及對人類喜歡的整齊的格式化數據進行轉換。爲了幫助您與這些雜事,Java 平臺提供了兩個API。scanning API 使用分隔符模式將其輸入分解爲標記。formatting API 將數據從新組合成格式良好的,人類可讀的形式。
默認狀況下,Scanner 使用空格字符分隔標記。(空格字符包括空格,製表符和行終止符。爲完整列表,請參閱 Character.isWhitespace)。示例 ScanXan 讀取 xanadu.txt 的單個詞語並打印他們:
public class ScanXan { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { Scanner s = null; try { s = new Scanner(new BufferedReader(new FileReader("resources/xanadu.txt"))); while (s.hasNext()) { System.out.println(s.next()); } } finally { if (s != null) { s.close(); } } } }
雖然 Scanner 不是流,但你仍然須要關閉它,以代表你與它的底層流執行完成。
調用 useDelimiter() ,指定一個正則表達式可使用不一樣的標記分隔符。例如,假設您想要標記分隔符是一個逗號,後面能夠跟空格。你會調用
s.useDelimiter(",\\s*");
該 ScanXan 示例是將全部的輸入標記爲簡單的字符串值。Scanner 還支持全部的 Java 語言的基本類型(除 char),以及 BigInteger 和 BigDecimal 的。此外,數字值可使用千位分隔符。所以,在一個美國的區域設置,Scanner 能正確地讀出字符串「32,767」做爲一個整數值。
這裏要注意的是語言環境,由於千位分隔符和小數點符號是特定於語言環境。因此,下面的例子將沒法正常在全部的語言環境中,若是咱們沒有指定 scanner 應該用在美國地區工做。可能你平時並不用關心,由於你輸入的數據一般來自使用相同的語言環境。可使用下面的語句來設置語言環境:
s.useLocale(Locale.US);
該 ScanSum 示例是將讀取的 double 值列表進行相加:
public class ScanSum { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { Scanner s = null; double sum = 0; try { s = new Scanner(new BufferedReader(new FileReader("resources/usnumbers.txt"))); s.useLocale(Locale.US); while (s.hasNext()) { if (s.hasNextDouble()) { sum += s.nextDouble(); } else { s.next(); } } } finally { s.close(); } System.out.println(sum); } }
輸出爲:1032778.74159
實現格式化流對象要麼是 字符流類的 PrintWriter 的實例,或爲字節流類的 PrintStream 的實例。
注:對於 PrintStream 對象,你極可能只須要 System.out 和 System.err。 (請參閱命令行I/O)當你須要建立一個格式化的輸出流,請實例化 PrintWriter,而不是 PrintStream。
像全部的字節和字符流對象同樣,PrintStream 和 PrintWriter 的實例實現了一套標準的 write 方法用於簡單的字節和字符輸出。此外,PrintStream 和 PrintWriter 的執行同一套方法,將內部數據轉換成格式化輸出。提供了兩個級別的格式:
調用 print 或 println 輸出使用適當 toString 方法變換後的值的單一值。咱們能夠看到這 Root 例子:
public class Root { /** * @param args */ public static void main(String[] args) { int i = 2; double r = Math.sqrt(i); System.out.print("The square root of "); System.out.print(i); System.out.print(" is "); System.out.print(r); System.out.println("."); i = 5; r = Math.sqrt(i); System.out.println("The square root of " + i + " is " + r + "."); } }
輸出爲:
The square root of 2 is 1.4142135623730951. The square root of 5 is 2.23606797749979.
在 i 和 r 變量格式化了兩次:第一次在重載的 print 使用代碼,第二次是由Java編譯器轉換碼自動生成,它也利用了 toString。您能夠用這種方式格式化任意值,但對於結果沒有太多的控制權。
該 format 方法用於格式化基於 format string(格式字符串) 多參。格式字符串包含嵌入了 format specifiers (格式說明)的靜態文本;除非使用了格式說明,不然格式字符串輸出不變。
格式字符串支持許多功能。在本教程中,咱們只介紹一些基礎知識。有關完整說明,請參閱 API 規範關於格式字符串語法。
Root2 示例在一個 format 調用裏面設置兩個值:
public class Root2 { /** * @param args */ public static void main(String[] args) { int i = 2; double r = Math.sqrt(i); System.out.format("The square root of %d is %f.%n", i, r); } }
輸出爲:The square root of 2 is 1.414214.
像本例中所使用的格式爲:
這裏有一些其餘的轉換格式:
還有許多其餘的轉換。
注意:除了 %%
和 %n
,其餘格式符都要匹配參數,不然拋出異常。在 Java 編程語言中,\ n
轉義老是產生換行符(\u000A
)。不要使用\ñ
除非你特別想要一個換行符。爲了針對本地平臺獲得正確的行分隔符,請使用%n
。
除了用於轉換,格式說明符能夠包含若干附加的元素,進一步定製格式化輸出。下面是一個 Format 例子,使用一切可能的一種元素。
public class Format { /** * @param args */ public static void main(String[] args) { System.out.format("%f, %1$+020.10f %n", Math.PI); } }
輸出爲:3.141593, +00000003.1415926536
附加元素都是可選的。下圖顯示了長格式符是如何分解成元素
元件必須出如今顯示的順序。從合適的工做,可選的元素是:
System.out.format(「%F,%<+ 020.10f%N」,Math.PI);
命令行 I/O 描述了標準流(Standard Streams)和控制檯(Console)對象。
Java 支持兩種交互方式:標準流(Standard Streams)和經過控制檯(Console)。
標準流是許多操做系統的一項功能。默認狀況下,他們從鍵盤讀取輸入和寫出到顯示器。它們還支持對文件和程序之間的 I/O,但該功能是經過命令行解釋器,而不是由程序控制。
Java平臺支持三種標準流:標準輸入(Standard Input, 經過 System.in 訪問)、標準輸出(Standard Output, 經過System.out 的訪問)和標準錯誤( Standard Error, 經過System.err的訪問)。這些對象被自動定義,並不須要被打開。標準輸出和標準錯誤都用於輸出;錯誤輸出容許用戶轉移常常性的輸出到一個文件中,仍然可以讀取錯誤消息。
您可能但願標準流是字符流,可是,因爲歷史的緣由,他們是字節流。 System.out 和System.err 定義爲 PrintStream 的對象。雖然這在技術上是一個字節流,PrintStream 利用內部字符流對象來模擬多種字符流的功能。
相比之下,System.in 是一個沒有字符流功能的字節流。若要想將標準的輸入做爲字符流,能夠包裝 System.in 在 InputStreamReader
InputStreamReader cin = new InputStreamReader(System.in);
更先進的替代標準流的是 Console 。這個單一,預約義的 Console 類型的對象,有大部分的標準流提供的功能,另外還有其餘功能。Console 對於安全的密碼輸入特別有用。Console 對象還提供了真正的輸入輸出字符流,是經過 reader 和 writer 方法實現的。
若程序想使用 Console ,它必須嘗試經過調用 System.console() 檢索 Console 對象。若是 Console 對象存在,經過此方法將其返回。若是返回 NULL,則 Console 操做是不容許的,要麼是由於操做系統不支持他們或者是由於程序自己是在非交互環境中啓動的。
Console 對象支持經過讀取密碼的方法安全輸入密碼。該方法有助於在兩個方面的安全。第一,它抑制迴應,所以密碼在用戶的屏幕是不可見的。第二,readPassword 返回一個字符數組,而不是字符串,因此,密碼能夠被覆蓋,只要它是再也不須要就能夠從存儲器中刪除。
Password 例子是一個展現了更改用戶的密碼原型程序。它演示了幾種 Console 方法
public class Password { /** * @param args */ public static void main(String[] args) { Console c = System.console(); if (c == null) { System.err.println("No console."); System.exit(1); } String login = c.readLine("Enter your login: "); char [] oldPassword = c.readPassword("Enter your old password: "); if (verify(login, oldPassword)) { boolean noMatch; do { char [] newPassword1 = c.readPassword("Enter your new password: "); char [] newPassword2 = c.readPassword("Enter new password again: "); noMatch = ! Arrays.equals(newPassword1, newPassword2); if (noMatch) { c.format("Passwords don't match. Try again.%n"); } else { change(login, newPassword1); c.format("Password for %s changed.%n", login); } Arrays.fill(newPassword1, ' '); Arrays.fill(newPassword2, ' '); } while (noMatch); } Arrays.fill(oldPassword, ' '); } // Dummy change method. static boolean verify(String login, char[] password) { // This method always returns // true in this example. // Modify this method to verify // password according to your rules. return true; } // Dummy change method. static void change(String login, char[] password) { // Modify this method to change // password according to your rules. } }
上面的流程是:
Data Streams 處理原始數據類型和字符串值的二進制 I/O。
支持基本數據類型的值((boolean, char, byte, short, int, long, float, 和 double)以及字符串值的二進制 I/O。全部數據流實現 DataInput 或 DataOutput 接口。本節重點介紹這些接口的普遍使用的實現,DataInputStream 和 DataOutputStream 類。
DataStreams 例子展現了數據流經過寫出的一組數據記錄到文件,而後再次從文件中讀取這些記錄。每一個記錄包括涉及在發票上的項目,以下表中三個值:
記錄中順序 | 數據類型 | 數據描述 | 輸出方法 | 輸入方法 | 示例值 |
---|---|---|---|---|---|
1 | double | Item price | DataOutputStream.writeDouble | DataInputStream.readDouble | 19.99 |
2 | int | Unit count | DataOutputStream.writeInt | DataInputStream.readInt | 12 |
3 | String | Item description | DataOutputStream.writeUTF | DataInputStream.readUTF | "Java T-Shirt" |
首先,定義了幾個常量,數據文件的名稱,以及數據。
static final String dataFile = "invoicedata"; static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 }; static final int[] units = { 12, 8, 13, 29, 50 }; static final String[] descs = { "Java T-shirt", "Java Mug", "Duke Juggling Dolls", "Java Pin", "Java Key Chain" };
DataStreams 打開一個輸出流,提供一個緩衝的文件輸出字節流:
out = new DataOutputStream(new BufferedOutputStream( new FileOutputStream(dataFile)))
DataStreams 寫出記錄並關閉輸出流:
for (int i = 0; i < prices.length; i ++) { out.writeDouble(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); }
該 writeUTF 方法寫出以 UTF-8 改進形式的字符串值。
如今,DataStreams 讀回數據。首先,它必須提供一個輸入流,和變量來保存的輸入數據。像 DataOutputStream 、DataInputStream 類,必須構形成一個字節流的包裝器。
in = new DataInputStream(new BufferedInputStream(new FileInputStream(dataFile))); double price; int unit; String desc; double total = 0.0;
如今,DataStreams 能夠讀取流裏面的每一個記錄,並在遇到它時將數據報告出來:
try { while (true) { price = in.readDouble(); unit = in.readInt(); desc = in.readUTF(); System.out.format("You ordered %d" + " units of %s at $%.2f%n", unit, desc, price); total += unit * price; } } catch (EOFException e) { }
請注意,DataStreams 經過捕獲 EOFException 檢測文件結束的條件而不是測試無效的返回值。全部實現了 DataInput 的方法都使用 EOFException 類來代替返回值。
還要注意的是 DataStreams 中的各個 write 須要匹配對應相應的 read。它須要由程序員來保證。
DataStreams 使用了一個很是糟糕的編程技術:它使用浮點數來表示的貨幣價值。在通常狀況下,浮點數是很差的精確數值。這對小數尤爲糟糕,由於共同值(如 0.1),沒有一個二進制的表示。
正確的類型用於貨幣值是 java.math.BigDecimal 的。不幸的是,BigDecimal 是一個對象的類型,所以它不能與數據流工做。然而,BigDecimal 將與對象流工做,而這部份內容將在下一節講解。
對象流處理對象的二進制 I/O。
正如數據流支持的是基本數據類型的 I/O,對象流支持的對象 I/O。大多數,但不是所有,標準類支持他們的對象的序列化,都須要實現 Serializable 接口。
對象流類包括 ObjectInputStream 和 ObjectOutputStream 的。這些類實現的 ObjectInput 與 ObjectOutput 的,這些都是 DataInput 和DataOutput 的子接口。這意味着,全部包含在數據流中的基本數據類型 I/O 方法也在對象流中實現了。這樣一個對象流能夠包含基本數據類型值和對象值的混合。該ObjectStreams 例子說明了這一點。ObjectStreams 建立與 DataStreams 相同的應用程序。首先,價格如今是 BigDecimal 對象,以更好地表明分數值。其次,Calendar 對象被寫入到數據文件中,指示發票日期。
public class ObjectStreams { static final String dataFile = "invoicedata"; static final BigDecimal[] prices = { new BigDecimal("19.99"), new BigDecimal("9.99"), new BigDecimal("15.99"), new BigDecimal("3.99"), new BigDecimal("4.99") }; static final int[] units = { 12, 8, 13, 29, 50 }; static final String[] descs = { "Java T-shirt", "Java Mug", "Duke Juggling Dolls", "Java Pin", "Java Key Chain" }; public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream out = null; try { out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(dataFile))); out.writeObject(Calendar.getInstance()); for (int i = 0; i < prices.length; i ++) { out.writeObject(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); } } finally { out.close(); } ObjectInputStream in = null; try { in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(dataFile))); Calendar date = null; BigDecimal price; int unit; String desc; BigDecimal total = new BigDecimal(0); date = (Calendar) in.readObject(); System.out.format ("On %tA, %<tB %<te, %<tY:%n", date); try { while (true) { price = (BigDecimal) in.readObject(); unit = in.readInt(); desc = in.readUTF(); System.out.format("You ordered %d units of %s at $%.2f%n", unit, desc, price); total = total.add(price.multiply(new BigDecimal(unit))); } } catch (EOFException e) {} System.out.format("For a TOTAL of: $%.2f%n", total); } finally { in.close(); } } }
若是的 readObject() 不返回預期的對象類型,試圖將它轉換爲正確的類型可能會拋出一個 ClassNotFoundException。在這個簡單的例子,這是不可能發生的,因此咱們不要試圖捕獲異常。相反,咱們通知編譯器,咱們已經意識到這個問題,添加 ClassNotFoundException 到主方法的 throws 子句中的。
writeObject 和 readObject 方法簡單易用,但它們包含了一些很是複雜的對象管理邏輯。這不像 Calendar 類,它只是封裝了原始值。但許多對象包含其餘對象的引用。若是 readObject 從流重構一個對象,它必須可以重建全部的原始對象所引用的對象。這些額外的對象可能有他們本身的引用,依此類推。在這種狀況下,writeObject 遍歷對象引用的整個網絡,並將該網絡中的全部對象寫入流。所以,writeObject 單個調用能夠致使大量的對象被寫入流。
以下圖所示,其中 writeObject 調用名爲 a 的單個對象。這個對象包含對象的引用 b和 c,而 b 包含引用 d 和 e。調用 writeObject(a) 寫入的不僅是一個 a,還包括全部須要從新構成的這個網絡中的其餘4個對象。當經過 readObject 讀回 a 時,其餘四個對象也被讀回,同時,全部的原始對象的引用被保留。
若是在同一個流的兩個對象引用了同一個對象會發生什麼?流只包含一個對象的一個拷貝,儘管它能夠包含任何數量的對它的引用。所以,若是你明確地寫一個對象到流兩次,實際上只是寫入了2此引用。例如,若是下面的代碼寫入一個對象 ob 兩次到流:
Object ob = new Object(); out.writeObject(ob); out.writeObject(ob);
每一個 writeObject 都對應一個 readObject, 因此從流裏面讀回的代碼以下:
Object ob1 = in.readObject(); Object ob2 = in.readObject();
ob1 和 ob2 都是相同對象的引用。
然而,若是一個單獨的對象被寫入到兩個不一樣的數據流,它被有效地複用 - 一個程序從兩個流讀回的將是兩個不一樣的對象。
本章例子的源碼,能夠在 https://github.com/waylau/essential-java 中 com.waylau.essentialjava.io
包下找到。