Java io功能總結分析(一):字節流讀寫


李青原(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異常,才會返回。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM