李青原(liqingyuan1986@aliyun.com)創作於博客園個人博客(http://www.cnblogs.com/liqingyuan/),轉載請標明出處。
java.io是javaSE提供的基礎功能,用以執行阻塞式的數據讀取和寫入操作。
java.io分為4個模塊,字節流的讀和寫、字節流的讀和寫,分別對應4個基礎抽象類:InputStream、OutputStream、Reader、Writer。
在這4個基礎類之上,java使用裝飾者模式,提供了面向多種應用場景的具體實現。本文主要是我對這些類個人學習分析的總結。
一.字節流讀寫:
1.UML:
2.代碼分析:
根據UML類圖,我按照4個模塊來分析:
基礎抽象類:定義基礎API,實現部分默認實現,針對一個字節讀寫的原子操作方法為抽象方法,由核心功能類實現。
核心功能實現類:針對內存和文件兩種場景,實現針對一個字節的原子操作。
裝飾功能實現類:在核心功能實現類基礎上,封裝了一些可選的裝飾功能。
特殊實現類:針對特殊場景和特殊數據設計的特殊類,但是最基本的原子操作依然由核心功能類實現。
(1)基礎抽象類:
OutputStream和InputStream分別定義了3個讀寫方法,查看源代碼會發現,3個方法都是以其中那個待實現的抽象方法為中心的,也就是write(int)和read()。
這個方法就是字節流讀寫的原子操作,針對一個字節的讀寫,將由核心功能類來實現。
區別在於,InputStream還規定了一種新的“標記和重置”功能,能夠隨時在流的當前讀取位置打標記(mark方法),然后使用reset方法可以在讀取到流的其他位置時重新回到標記位置,同時提供了markSupported方法供查看流對象是否支持此操作。
不過InputStream本身只是定義了這些API,並未實現,markSupported方法返回的是false,需要子類自己實現。
注意事項:
①write(int)的參數是4個字節的int類型,實現類在寫入時會強轉為1個字節的byte類型,這個強轉會發生去掉溢出的3個字節,比如write(256),實際寫入的是0x00,也就等效於write(0);
②read()方法返回的是4字節的int類型,API規定了這個方法會把讀取的字節的高字節位置補零3個字節,因此如果讀到數據,返回值必然在0到255之間,如果未讀到返回-1;
③InputStream提供標記和重置功能API,是否支持由子類決定。
(2)核心功能實現類:
所謂的核心功能實現類,也就是實現了最基礎的寫入功能——實現了原子操作write(int)和read()。
A.內存讀寫:
ByteArrayOutputStream實現內存字節流的寫入。
它內部持有一個數組,在構造方法中被初始化,默認是32長度,也可以通過構造方法指定長度。
如何獲得寫入結果呢?
原來,為了保證數據安全,ByteArrayOutputStream內部持有的數組是私有的,但它為我們提供了獲得寫入結果的方法:toByteArray(),獲得內部數組的一個克隆,也可以使用toString方法獲得編碼后的字符流。
示例:
public static void main(String[] args) throws UnsupportedEncodingException, IOException { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); byte[] bytes = "io測試".getBytes("utf-8"); byteStream.write(bytes); System.out.println(new String(byteStream.toByteArray(),"utf-8"));//io測試 System.out.println(byteStream.toString("utf-8"));//io測試 }
ByteArrayInputStream實現內存字節流的讀取。
因此ByteArrayInputStream構造方法必須至少有一個參數,傳入數據來源byte數組;此核心類支持“標記和重置”功能。
示例:
public static void main(String[] args){ byte[] sourceData = {1,2,3,4,5}; ByteArrayInputStream inputStream = new ByteArrayInputStream(sourceData); byte[] targetData = new byte[5]; inputStream.read(targetData, 0, 3); //從第一位讀3個,分別是 1 2 3 inputStream.mark(-1); //打標記,mark方法的參數對ByteArrayInputStream無意義,可以隨便傳 targetData[3] = (byte)inputStream.read(); //繼續讀第四個數,4 inputStream.reset(); //重置回標記位置,也就是3處 targetData[4] = (byte)inputStream.read(); //從標記出再開始讀,依然是第四個數,4 System.out.println(Arrays.toString(targetData));//顯示1,2,3,4,4 }
B.文件讀寫:
FileOutputStream實現文件寫入。
寫入模式分為兩種:覆蓋或者追加,模式被構造方法的參數指定,默認是覆蓋寫入。
示例:
public static void main(String[] args) throws UnsupportedEncodingException, IOException { FileOutputStream fileStream = new FileOutputStream("d:/123.txt");//此時文件已經清空 fileStream.write(256);//256超出了byte類型的范圍,發生溢出,實際寫入的是0x00 fileStream.close();//釋放系統文件資源和關閉FileChannel對象 }
FileInputStream實現文件讀取。
因此構造方法必須至少有一個參數,指定數據來源文件,可以是文件對象,也可以是文件路徑,還可以是FileDescriptor對象;此核心類不支持“標記和重置”功能。
示例:
public static void main(String[] args) throws IOException{ FileOutputStream outputStream = new FileOutputStream("d:/123.txt"); outputStream.write(new byte[]{1,2,3,4,5}); FileInputStream inputStream = new FileInputStream("d:/123.txt"); byte[] targetData = new byte[4]; inputStream.read(targetData);//填滿targetData,4個字節 System.out.println(inputStream.available());//寫了4個字節,文件剩余可寫數據為1 inputStream.close();//關閉文件資源 System.out.println(Arrays.toString(targetData));//1,2,3,4 }
注意事項:
①內存讀寫已經保證了多線程安全,但是文件讀寫沒有保證;
②內存讀寫不需要調用close方法;而文件讀寫使用文件系統,必須調用close方法釋放文件資源;
③字節流核心寫入類都不帶緩存功能,因此直接使用時無需調用flush方法;
④FileOutputStream對象在創建時,就會執行一個native的open操作,如果是覆蓋模式,此操作會立刻清除原文件內容,因此創建覆蓋模式的FileOutputStream對象要非常謹慎,就算我們沒做任何寫入操作,文件內容也會被覆蓋。
⑤ByteArrayInputStream支持標記和重置功能,FileInputStream不支持;
⑥ByteArrayInputStream的mark(int)方法中,參數無意義,隨便傳。
(3)裝飾功能類:
裝飾類本身並實現單字節寫入的原子操作,而是通過持有2個核心類之一的對象來獲得這種能力,並同時提供額外的附加功能。
A.裝飾類父類:
作為裝飾父類,FilterOutputStream和FilterInputStream最重要的作用就是把所有InputStream規定的API轉到內部持有的InputStream對象上,本身只是一個很簡單的封裝層。
注意事項:
FilterOutputStream在close方法中默認調用flush方法,這樣在使用字節流寫入的裝飾子類時,無需在關閉前調用flush方法了。
B.裝飾功能——緩存讀寫:
BufferedOutputStream實現帶緩存功能的字節流寫入。
BufferedOutputStream通過內部持有一個字節數組來緩存外部請求寫入的數據,以下三種情況,它都會清空數組並真正的把數據寫入目標:
緩存數組已滿;
緩存數組剩余空間不足以緩存新的請求寫入的數據;
flush或者close方法被調用時。
BufferedInputStream實現帶緩存功能的字節流讀取。
BufferedInputStream的緩存功能實現要復雜很多,主要是以下幾個方面:
BufferedInputStream構造方法中指定的緩存大小,只是初始大小;
BufferedInputStream的緩存數組有一套“可伸縮”機制(參見源碼fill方法);
BufferedInputStream在以上基礎上實現了基於緩存數組的標記和重置功能。
考慮到內存讀取和IO讀取的消耗區別,BufferedInputStream通常是用來提升FileInputStream的讀取性能的,而FileInputStream並未在JVM層面實現標記和重置功能,BufferedInputStream則在代碼層面完善了這一功能。
注意事項:
①緩存讀寫的裝飾類都額外保證了讀寫操作的多線程安全性;
②緩存讀寫的緩存大小默認都是8KB,可以通過構造方法指定,但讀取操作的緩存是可變的,使用中大小可能發生變化;
③緩存讀取還附帶實現了標記和重置功能,不同於ByteArrayInputStream的標記重置功能,BufferedInputStream讀取提供的mark(int)的參數有意義,指定了標記在多少個字節內是起效的。
示例:
public static void main(String[] args) throws IOException{ /* * 緩存寫入示范 */ BufferedOutputStream bufferOutput = new BufferedOutputStream(new FileOutputStream("d:/123.txt")); bufferOutput.write("IO測試".getBytes("utf-8")); bufferOutput.close();//裝飾類的close方法會先調用flush方法,所以無需再調用flush() /* * 緩存讀取示范 */ FileOutputStream outputStream = new FileOutputStream("d:/123.txt"); outputStream.write(new byte[]{1,2,3,4,5,6}); FileInputStream inputStream = new FileInputStream("d:/123.txt"); BufferedInputStream bufferInput = new BufferedInputStream(inputStream); byte[] targetArray = new byte[6]; bufferInput.mark(targetArray.length);//在最開始位置標記 bufferInput.read(targetArray,0,3);//讀取三個字節 123 bufferInput.reset();//重置,回到標記位置 bufferInput.read(targetArray,3,3);//再讀取三個字節 還是123 bufferInput.close(); for(byte b : targetArray){ System.out.print(b);//循環輸出為123123 } }
C.裝飾功能——JAVA基本類型讀寫:
DataOutputStream:提供JAVA基本類型的寫入API,可以實現把JAVA基本類型的二進制數據寫入目標,同時提供獲得已寫入數據字節大小的API。
DataInputStream:提供JAVA基礎類型的讀取API,會把讀取的字節轉為對應類型的數據,根據類型的長度,會選擇連續操作幾次原子操作。
注意事項:
①JAVA基本類型讀寫的API都沒有保證多線程安全,需要外部調用者自己保證;
②DataOutputStream的基本寫入(3個write方法)額外保證了多線程安全,而DataInputStream的基本讀取(3個read方法)則沒有額外保證,完全取決於內部持有的核心實現類;
③DataOutputStream的size()方法最大只能提供2的31次方減1,超過這個依然只顯示這個數值。
示例:
public static void main(String[] args) throws IOException { /* * JAVA基本類型寫入示范 */ DataOutputStream dataOutput = new DataOutputStream(new FileOutputStream("d:/123.txt")); dataOutput.writeInt(-1);//文件中寫入的16進制數據是 FF FF FF FF,也就是32個1(-1的補碼) dataOutput.writeBoolean(true);//文件中寫入的16進制數據是 01 System.out.println(dataOutput.size());;//已寫字節,4+1 已寫5字節,最大只能記錄Integer.MAX_VALUE dataOutput.close(); /* * JAVA基本類型讀取示范 */ FileOutputStream outputStream = new FileOutputStream("d:/123.txt"); outputStream.write(new byte[]{0x00,0x31});//寫入 0x0031,也就是49 FileInputStream inputStream = new FileInputStream("d:/123.txt"); DataInputStream dataInput = new DataInputStream(inputStream); System.out.println(dataInput.readChar());//顯示1 DataInputStream會連續讀取2個字節,0x0031轉為字符也就是'1' dataInput.close(); }
D.裝飾功能——字符輸出打印:
PrintStream是一個非常特殊的類,它雖然和Writer的子類一樣能打印字符流,但是它的底層卻是以字節為原子操作的,效率上不如PrintWriter,但是它依然能操作字節寫入,這點上又更靈活。
作為打印類,PrintStream提供換行功能,除了調用flush和println類方法外,它還能自動識別換行字符'\n'(這點PrintWriter做不到);同時PrintStream也不會拋出IO異常,而是通過checkError方法獲得是否存在錯誤。
注意事項:
①PrintStream能夠識別換行字符'\n',PrintWriter不能;
②System.out就是一個PrintStream對象;
③PrintStream額外保證了所有API的多線程安全。
示例:
public static void main(String[] args) throws UnsupportedEncodingException, IOException { PrintStream printStream = new PrintStream (new BufferedOutputStream(new FileOutputStream("d:/123.txt")));//封裝了緩存功能的打印流 printStream.print("IO測試\n");//包含換行符,此行已經輸入文件 printStream.print("IO測試");//此行還未輸入 System.out.println(printStream.checkError());//無錯誤,顯示false printStream.close();//關閉時默認調用了flush方法,第二行也被輸入 }
E.裝飾功能——字節流讀取時的回退功能:
PushbackInputStream:提供回退功能。
PushbackInputStream通過內部一個回退區的緩存數組,可以讓我們把已經讀出來的數據回寫到流中,以供其他使用者或者下次讀取使用。
注意事項:
①PushbackInputStream可以設置回填區的大小(默認1),如果回填數據長度超過設置值,會拋出IO異常;
②PushbackInputStream沒有提供額外多線程安全保證,完全取決於內部持有的核心實現類。
示例:
public static void main(String[] args) throws IOException{ FileOutputStream outputStream = new FileOutputStream("d:/123.txt"); outputStream.write(new byte[]{1,2,3,4,5,6}); FileInputStream inputStream = new FileInputStream("d:/123.txt"); PushbackInputStream pushbackStream = new PushbackInputStream(inputStream); System.out.print(pushbackStream.read()); System.out.print(pushbackStream.read()); System.out.print(pushbackStream.read()); System.out.print(pushbackStream.read()); System.out.print(pushbackStream.read()); System.out.print(pushbackStream.read());//讀完六次 分別是123456 System.out.print(pushbackStream.read());//無數據,顯示-1 pushbackStream.unread(0);//回填一個0 System.out.print(pushbackStream.read());//讀出0 pushbackStream.close(); }
F.裝飾功能——字節流讀取時的行數計數功能:
LineNumberInputStream:一個過時的提供行數計算的裝飾類,這個類是JAVA SE中罕見的存在錯誤的類,主要是把0x0A這個字節錯誤的和'\n'換行符等同處理了,'\r'也存在類似問題。
注意事項:
LineNumberInputStream在某些情況下存在錯誤,應當使用LineNumberReader替代。
錯誤示例:
@SuppressWarnings("deprecation") public static void main(String[] args) throws IOException{ FileOutputStream outputStream = new FileOutputStream("d:/123.txt"); outputStream.write(new byte[]{0x0a}); FileInputStream inputStream = new FileInputStream("d:/123.txt"); LineNumberInputStream lineStream = new LineNumberInputStream(inputStream); System.out.println(lineStream.getLineNumber());//0 lineStream.read(); System.out.println(lineStream.getLineNumber());//1 很明顯錯誤,0x0a並不一定是換行符,換行符應該是0x000a才對 lineStream.close(); }
(4)特殊實現:
A.序列化對象二進制流的讀寫:
ObjectOutputStream和ObjectInputStream依然通過核心類來實現字節寫入的原子操作,但是不再直接參與裝飾者模式,而是通過內部類來持有一個JAVA基本類型讀寫裝飾類。
由於目標不再是普通字節流,而是特殊的序列化對象的二進制字節流,所以它們的API也不一樣,提供了針對各種類型的讀取API。
注意事項:
①ObjectOutputStream為了提高序列化效率,在序列化對象時,會通過內存地址來判斷是否為同一個對象,重復對象的序列化會用引用來代替;
②規則①對於對象內部的對象一樣適用;
③使用writeUnshared方法可以強制要求不使用引用方式序列化,但是對於對象內部的對象此方法無效;
④ObjectInputStream在讀取時,同一類型數據會依次讀取,比如連續2次調用readObject()會依次獲得第一次寫入和第二次寫入的Object對象。
示例:
@SuppressWarnings("deprecation") public static void main(String[] args) throws UnsupportedEncodingException, IOException, ClassNotFoundException { ObjectOutputStream objectOutput = new ObjectOutputStream(new FileOutputStream("d:/123.txt")); Date a = new Date("2013/01/01"); objectOutput.writeObject(a);//第一次寫入對象本身 a.setHours(12); objectOutput.writeObject(a);//第二次寫入,是重復對象,無視修改,依然寫入引用 objectOutput.writeUnshared(a);//第三次寫入,不管是否重復都寫入對象 objectOutput.close(); ObjectInputStream objectInput = new ObjectInputStream(new FileInputStream("d:/123.txt")); System.out.println(objectInput.readObject());//Tue Jan 01 00:00:00 CST 2013 System.out.println(objectInput.readObject());//Tue Jan 01 00:00:00 CST 2013 第二次寫入修改未起效,因為是引用 System.out.println(objectInput.readObject());//Tue Jan 01 12:00:00 CST 2013 第三次寫入修改起效 }
B.多來源讀取:
SequenceInputStream用來從多個數據來源,讀取字節流,當一個來源讀取到末尾,會自動切換到下個數據來源。
示例:
public static void main(String[] args) throws IOException{ FileOutputStream outputStream = new FileOutputStream("d:/123.txt"); outputStream.write(new byte[]{1,2,3});//第一個文件寫入123 FileOutputStream outputStream2 = new FileOutputStream("d:/456.txt"); outputStream2.write(new byte[]{4,5,6});//第二個文件寫入456 FileInputStream inputStream = new FileInputStream("d:/123.txt"); FileInputStream inputStream2 = new FileInputStream("d:/456.txt"); InputStream[] array = new InputStream[]{inputStream,inputStream2}; ArrayEnumeration inputEnum = new ArrayEnumeration(array); SequenceInputStream sequenceStream = new SequenceInputStream(inputEnum); byte[] target = new byte[6]; for(int i = 0; i < target.length; i++){ System.out.println(sequenceStream.read());//循環輸出123456 } sequenceStream.close(); }
注意事項:SequenceInputStream存在一個BUG,如果調用多字節讀取的方法,自動切換功能會失效。
錯誤示例:
public static void main(String[] args) throws IOException{ FileOutputStream outputStream = new FileOutputStream("d:/123.txt"); outputStream.write(new byte[]{1,2,3});//第一個文件寫入123 FileOutputStream outputStream2 = new FileOutputStream("d:/456.txt"); outputStream2.write(new byte[]{4,5,6});//第二個文件寫入456 FileInputStream inputStream = new FileInputStream("d:/123.txt"); FileInputStream inputStream2 = new FileInputStream("d:/456.txt"); InputStream[] array = new InputStream[]{inputStream,inputStream2}; ArrayEnumeration inputEnum = new ArrayEnumeration(array); SequenceInputStream sequenceStream = new SequenceInputStream(inputEnum); byte[] target = new byte[6]; sequenceStream.read(target); for(byte b:target){ System.out.println(b);//錯誤顯示123000 而不是123456 } }
查看源碼,發現SequenceInputStream內部的錯誤:
D.字符串讀取操作:
StringBufferInputStream也是一個過時的類,由於不恰當的混淆了雙字節的字符類型和單字節的byte類型,讀取的數據並不能反映真正的字符二進制數據。
注意事項:
應當使用StringReader替代StringBufferInputStream。
E.管道讀寫操作:
PipedOutputStream必須和PipedInputStream一一對應的使用,主要是用作線程之間的數據轉移,具體的使用將在IO篇的后續第三篇文章中詳細介紹。
3.總結:
(1)字節流的讀寫在JAVA的最初版本就已經提供,但讀取操作中,個別讀取類不恰當的處理了字符和字節的關系,導致存在BUG,應當使用1.1版本以后提供的字符流替代類來操作。
(2)由於使用裝飾者模式,很多時候我們可以嵌套核心類和裝飾類來使用,這樣構建的對象應該同時小心核心類和裝飾類的注意事項。
(3)多線程環境下,內存字節流讀寫本身已經保證了多線程安全;
而文件字節流讀寫則沒有保證,但如果使用緩存讀寫裝飾類封裝,則能夠通過緩存讀寫裝飾類來保證多線程安全,前提是一個文件讀寫類只被一個緩存讀寫裝飾類使用;
裝飾類提供的JAVA基本數據類型的各種讀取API,都沒有保證線程安全性,需要調用者自己保證。
(4)IO功能中的字節流讀寫都是阻塞式的,會一直等到操作成功或者拋出IO異常,才會返回。