Java中常見的IO流及其使用

孫群發表於2015-07-09

Java中IO流分成兩大類,一種是輸入流,所有的輸入流都直接或間接繼承自InputStream抽象類,輸入流作為資料的來源,我們可以通過輸入流的read方法讀取位元組資料;另一種是輸出流,所有的輸出流都直接或間接繼承自OutputStream抽象類,輸出流接收資料,可以通過write方法寫入位元組資料。在Java的IO流類中,大部分的輸入流和輸出流都是成對存在的,即如果存在XXXInputStream,那麼就存在XXXOutputStream,反之亦然。(SequenceInputStream和StringBufferInputStream是特例,沒有對應的SequenceOutputStream類和StringBufferOutputStream類,稍後會解釋)。許多IO操作都可能會丟擲IOException異常,比如read、write、close操作。
以下是Java的IO流中常見的輸入流,由於每個輸入流都有其對應的輸出流,所以此處就不再列出輸出流的繼承結構圖。

Java中常見IO流

下面依次對這些類進行介紹以及如何使用。


ByteArrayInputStream & ByteArrayOutputStream

ByteArrayInputStream建構函式中需要傳入一個byte陣列作為資料來源,當執行read操作時,就會從該陣列中讀取資料,正如其名,是一種基於位元組陣列實現的一種簡單輸入流,顯而易見的是,如果在建構函式中傳入了null作為位元組資料,那麼在執行read操作時就會出現NullPointerException異常,但是在建構函式初始化階段不會丟擲異常;與之相對應的是ByteArrayOutputStream,其內部也有一個位元組陣列用於儲存write操作時寫入的資料,在建構函式中可以傳入一個size指定其內部的byte陣列的大小,如果不指定,那麼預設它會將byte陣列初始化為32位元組,當持續通過write向ByteArrayOutputStream中寫入資料時,如果其內部的byte陣列的剩餘空間不能夠儲存需要寫入的資料,那麼那麼它會通過呼叫內部的ensureCapacity
方法對其內部維護的byte陣列進行擴容以儲存所有要寫入的資料,所以不必擔心其內部的byte陣列太小導致的IndexOutOfBoundsException之類的異常。
以下是ByteArrayInputStream 和 ByteArrayOutputStream的程式碼片段示例:

private static void testByteArrayInputOutStream(){
        byte[] bytes = "I am iSpring".getBytes();
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        byte[] buf = new byte[1024];

        int length = 0;

        try{
            while((length = bais.read(buf)) > 0){
                baos.write(buf, 0, length);
            }
            System.out.println(baos.toString("UTF-8"));
            bais.close();
            baos.close();
        }catch(IOException e){
            e.printStackTrace();
        }
    }

在上面的例子中,我們通過字串獲取位元組陣列將其作為ByteArrayInputStream的資料流來源,然後通過讀取ByteArrayInputStream的資料,將讀到的資料寫入到ByteArrayOutputStream中。


FileInputStream & FileOutputStream

FileInputStream 能夠將檔案作為資料來源,讀取檔案中的流,通過File物件或檔案路徑等初始化,在其建構函式中,如果傳入的File物件(或與其相對應的檔案路徑所表示的File物件)不存在或是一個目錄而不是檔案或者由於其他原因無法開啟讀取資料,都會導致在初始化階段導致丟擲FileNotFoundException異常;與FileInputStream 相對應的是FileOutputStream,可以通過FileOutputStream向檔案中寫入資料,也需要通過File物件或檔案路徑對其初始化,如同FileInputStream ,如果傳入的File物件(或與其相對應的檔案路徑所表示的File物件)是一個目錄而不是檔案或者由於其他原因無法建立該檔案寫入資料,都會導致在初始化階段丟擲FileNotFoundException異常。
以下是FileInputStream 和 FileOutputStream的程式碼示例片段:

private static void testFileInputOutStream(){
        try{
            String inputFileName = "D:\\iWork\\file1.txt";
            String outputFileName = "D:\\iWork\\file2.txt";
            FileInputStream fis = new FileInputStream(inputFileName);
            FileOutputStream fos = new FileOutputStream(outputFileName);
            byte[] buf = new byte[1024];
            int length = 0;
            while ((length = fis.read(buf)) > 0){
                fos.write(buf, 0, length);
            }
            fis.close();
            fos.close();
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

在上面的例子中,我們通過FileInputStream的read方法讀取file1.txt中的資料,然後將獲得的位元組資料通過FileOutputStream的write方法將其寫入到另一個檔案file2.txt中,這樣就實現了檔案的拷貝,即將file1.txt拷貝到file2.txt。如果file2.txt已經存在,那麼在初始FileOutputStream時,可以傳入一邊boolean變數append表示是向已有檔案中追加寫入資料還是覆蓋已有資料。


PipedInputStream & PipedOutputStream

PipedInputStream和PipedOutputStream一般是結合使用的,這兩個類用於在兩個執行緒間進行管道通訊,一般在一個執行緒中執行PipedOutputStream 的write操作,而在另一個執行緒中執行PipedInputStream的read操作。可以在建構函式中傳入相關的流將PipedInputStream 和PipedOutputStream 繫結起來,也可以通過二者的connect方法將二者繫結起來,一旦二者進進行了繫結,那麼PipedInputStream的read方法就會自動讀取PipedOutputStream寫入的資料。PipedInputStream的read操作是阻塞式的,當執行PipedOutputStream的write操作時,PipedInputStream會在另一個執行緒中自動讀取PipedOutputStream寫入的內容,如果PipedOutputStream一直沒有執行write操作寫入資料,那麼PipedInputStream的read方法會一直阻塞PipedInputStream的read方法所執行的執行緒直至讀到資料。單獨使用PipedInputStream或單獨使用PipedOutputStream時沒有任何意義的,必須將二者通過connect方法(或在建構函式中傳入對應的流)進行連線繫結,如果單獨使用其中的某一個類,就會觸發IOException: Pipe Not Connected.
以下是PipedInputStream和PipedOutputStream的程式碼示例片段:

WriterThread類

import java.io.*;

public class WriterThread extends Thread {

    PipedOutputStream pos = null;

    public WriterThread(PipedOutputStream pos){
        this.pos = pos;
    }

    @Override
    public void run() {
        String message =  "這條資訊來自於WriterThread.";

        try{
            byte[] bytes = message.getBytes("UTF-8");
            System.out.println("WriterThread傳送資訊");
            this.pos.write(bytes);
            this.pos.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

ReaderThread類

import java.io.*;

public class ReaderThread extends Thread {
    private PipedInputStream pis = null;

    public ReaderThread(PipedInputStream pis){
        this.pis = pis;
    }

    @Override
    public void run() {
        byte[] buf = new byte[1024 * 8];

        try{
            System.out.println("ReaderThread阻塞式的等待接收資料...");
            int length = pis.read(buf);
            System.out.println("ReaderThread接收到如下資訊:");
            String message = new String(buf, 0, length, "UTF-8");
            System.out.println(message);
            pis.close();
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

測試程式碼

private  static void testPipedInputOutputStream(){
        try{
            PipedInputStream pis = new PipedInputStream();
            PipedOutputStream pos = new PipedOutputStream();
            pos.connect(pis);
            WriterThread writerThread = new WriterThread(pos);
            ReaderThread readerThread = new ReaderThread(pis);
            readerThread.start();
            writerThread.start();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

在上面的例項中,我們建立了兩個執行緒類WriterThread和ReaderThread,在WriterThread的建構函式中我們傳入了一個PipedOutputStream,並線上程執行run方法時向WriterThread中寫入資料;在ReaderThread的建構函式中我們傳入了一個PipedInputStream,在其執行緒執行run方法時阻塞式的執行read操作,等待獲取資料。我們通過pos.connect(pis)將這兩種流繫結在一起,最後分別執行執行緒ReaderThread和WriterThread。
輸出結果如下:
輸出結果

我們可以看到即使我們先執行了ReaderThread執行緒,ReaderThread中的PipedInputStream還是一直在阻塞式的等待資料的到來。


ObjectInputStream & ObjectOutputStream

ObjectOutputStream具有一系列writeXXX方法,在其建構函式中可以摻入一個OutputStream,可以方便的向指定的輸出流中寫入基本型別資料以及String,比如writeBoolean、writeChar、writeInt、writeLong、writeFloat、writeDouble、writeCharts、writeUTF等,除此之外,ObjectOutputStream還具有writeObject方法。writeObject方法中傳入的型別必須實現了Serializable介面,從而在執行writeObject操作時將物件進行序列化成流,並將其寫入指定的輸出流中。與ObjectOutputStream相對應的是ObjectInputStream,ObjectInputStream有與OutputStream中的writeXXX系列方法完全對應的readXXX系列方法,專門用於讀取OutputStream通過writeXXX寫入的資料。
以下是ObjectInputStream 和 ObjectOutputStream的示例程式碼:

Person類

import  java.io.Serializable;

public class Person implements Serializable {
    private String name = "";
    private int age = 0;

    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

測試程式碼

private static void testObjectInputOutputStream(){
        try{
            String fileName = "D:\\iWork\\file.tmp";
            //將記憶體中的物件序列化到物理檔案中
            FileOutputStream fos = new FileOutputStream(fileName);
            ObjectOutputStream oos = new ObjectOutputStream(fos);

            String description = "以下是人員陣列";

            Person[] persons = new Person[]{
                    new Person("iSpring", 26),
                    new Person("Mr.Sun", 27),
                    new Person("Miss.Zhou", 27)
            };

            oos.writeObject(description);

            oos.writeInt(persons.length);

            for(Person person : persons){
                oos.writeObject(person);
            }

            oos.close();

            //從物理檔案中反序列化讀取物件資訊
            FileInputStream fis = new FileInputStream(fileName);
            ObjectInputStream ois = new ObjectInputStream(fis);
            String str = (String)ois.readObject();
            System.out.println(str);
            int personCount = ois.readInt();
            for(int i = 0; i < personCount; i++){
                Person person = (Person)ois.readObject();
                StringBuilder sb = new StringBuilder();
                sb.append("姓名: ").append(person.getName()).append(", 年齡: ").append(person.getAge());
                System.out.println(sb);
            }
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch(IOException e){
            e.printStackTrace();
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }

輸出結果如下:
測試結果
Person實現了Serializable介面,需要注意的是,Serializable介面是一個標識介面,並不需要實現任何方法。我們首先通過ObjectOutputStream,將Person等陣列資訊序列化成流,然後通過呼叫writeObject等方法將其寫入到FileOutputStream中,從而實現了將記憶體中的基本型別和物件序列化儲存到硬碟的物理檔案中。然後通過FileInputStream讀取檔案,將檔案的輸入流傳入到到ObjectInputStream的建構函式中,這樣ObjectInputStream就可以通過執行對應的readXXX操作讀取基本型別或物件。當執行readObject操作時,返回的是Object型別,需要強制轉換為對應的實際型別。需要注意的是,ObjectInputStream執行readXXX操作的方法順序需要與ObjectOutputStream執行writeXXX操作的方法順序一致,否則就會讀到錯誤的資料或丟擲異常,比如一開始向FileOutputStream中執行writeFloat,而在FileInputStream中首先執行了readInt操作,不會報錯,因為writeFloat寫入了4個位元組的資料,readInt讀入了4個位元組的資料,雖然可以將該Float轉換為對應的int,但是其實已經不是我們想要的資料了,所以要注意readXXX操作與writeXXX操作執行順序的對應。


SequenceInputStream

SequenceInputStream 主要是將兩個(或多個)InputStream在邏輯上合併為一個InputStream,比如在建構函式中傳入兩個InputStream,分別為in1和in2,那麼SequenceInputStream在讀取操作時會先讀取in1,如果in1讀取完畢,就會接著讀取in2。在我們理解了SequenceInputStream 的作用是將兩個輸入流合併為一個輸入流之後,我們就能理解為什麼不存在對應的SequenceOutputStream 類了,因為將一個輸出流拆分為多個輸出流是沒有意義的。
以下是關於SequenceInputStream的示例程式碼:

private static void testSequenceInputOutputStream(){
        String inputFileName1 = "D:\\iWork\\file1.txt";
        String inputFileName2 = "D:\\iWork\\file2.txt";
        String outputFileName = "D:\\iWork\\file3.txt";

        try{
            FileInputStream fis1 = new FileInputStream(inputFileName1);
            FileInputStream fis2 = new FileInputStream(inputFileName2);
            SequenceInputStream sis = new SequenceInputStream(fis1, fis2);
            FileOutputStream fos = new FileOutputStream(outputFileName);
            byte[] buf = new byte[1024];
            int length = 0;
            while((length = sis.read(buf)) > 0){
                fos.write(buf, 0, length);
            }
            sis.close();
            fos.close();
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

我們通過FileInputStream分別獲取了file1.txt和file2.txt的輸入流,然後將這兩個輸入流作為建構函式的引數建立了SequenceInputStream 的例項,所以該SequenceInputStream 中已經在邏輯上將file1.txt和file2.txt的內容合併為了一個輸入流,然後我們讀取該SequenceInputStream 中的資料,並將讀到的資料寫入到一個新的FileOutputStream中,這樣我們就實現了將file1.txt和file2.txt合併為一個新的檔案file3.txt,原有的file1.txt和file2.txt檔案不受任何影響。


StringBufferInputStream

StringBufferInputStream允許通過在建構函式中傳入字串以讀取位元組,在讀取時內部主要呼叫了String的charAt方法。與SequenceInputStream類似,StringBufferInputStream也沒有對應的OutputStream,即不存在StringBufferOutputStream類。Java沒有設計StringBufferOutputStream類的理由也很簡單,我們假設StringBufferOutputStream存在,那麼StringBufferOutputStream應該是內部通過執行write操作寫入資料更新其內部的String物件,比如有可能是通過StringBuilder來實現,但是這樣做毫無意義,因為一旦我們String的建構函式中可以直接傳入位元組陣列構建字串,簡單明瞭,所以設計StringBufferOutputStream就沒有太大的必要了。StringBufferInputStream這個類本身存在一點問題,它不能很好地將字元陣列轉換為位元組陣列,所以該類被Java標記為廢棄的(Deprecated),其官方推薦使用StringReader作為代替。
以下是關於StringBufferInputStream的示例程式碼:

private static void testStringBufferInputStream(){
        String message = "I am iSpirng.";
        StringBufferInputStream sbis = new StringBufferInputStream(message);
        byte[] buf = new byte[1024];
        try{
            int length = sbis.read(buf);
            if(length > 0){
                System.out.println(new String(buf, 0, length, "UTF-8"));
            }
            sbis.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

輸出結果如下:

輸出結果


FilterInputStream & FilterOutputStream

FilterInputStream包含了其他的輸入流,說具體點就是在其建構函式中需要傳入一個InputStream並將其儲存在其名為in的欄位中,FilterInputStream只是簡單的覆蓋了所有的方法,之所說是簡單覆蓋是因為在每個覆蓋函式中,它只是呼叫內部的儲存在in欄位中的InputStream所對應的方法,比如在其覆蓋read方法時,內部只是簡單呼叫了in.read()方法。FilterInputStream的子類可以進一步覆蓋某些方法以保持介面不變的情況下實現某一特性(比如其子類有的可以通過使用快取優化讀取的效率)或者提供一些其他額外的實用方法。所以在使用時FilterInputStream可以讓傳入的InputStream具有一些額外的特性,即對建構函式傳入的InputStream進行了一層包裹,使用了典型的裝飾著模式,如果只看FilterInputStream本身這一個類的話,則該類自己本身意義不大,因為其只是通過內部的欄位in簡單覆寫某些方法。但是如果將FilterInputStream 和其子類結合起來使用話,那麼就很有用了。比如FilterInputStream 有兩個子類BufferedInputStream和DataInputStream,這兩個類在下面還會詳細介紹。BufferedInputStream對read操作做了優化,每次讀操作時都讀取一大塊資料,然後將其放入內部維護的一個位元組陣列緩衝區中。當外面呼叫BufferedInputStream的read方法時,首先去該緩衝區中讀取資料,這樣就避免了頻繁的實際的讀操作,BufferedInputStream對外沒有暴露額外的其他方法,但是其內部的read方法已經經過優化了,所以在執行讀操作的時候效率更高。DataInputStream與ObjectInputStream有點類似,可以通過一些readXXX方法讀取基本型別的資料,這是非常有用的一些方法。假設我們即想使用BufferedInputStream讀取效率高的特性,又想是想DataInputStream中的readXXX方法怎麼辦呢?很簡單,如下程式碼所示:

InputStream is = getInputStreamBySomeway();
BufferedInputStream bis = new BufferedInputStream(is);
DataInputStream dis = new DataInputStream(bis);

然後我們就可以呼叫dis.readXXX()等方法,即快又方便,這就是FilterInputStream子類通過建構函式層層傳遞結合在一起使用多種特性的魅力。與之相對應的是BufferedOutputStream和DataOutputStream,BufferedOutputStream優化了write方法,提高了寫的效率,DataOutputStream具有很多writeXXX方法,可以方便的寫入基本型別資料。如果想使用writeXXX方法,還想提高寫入到效率,可以如下程式碼使用,與上面的程式碼差不多:

OutputStream os = getOutputStreamBySomeway();
BufferedOutputStream bos = new BufferedOutputStream();
DataOutputStream dos = new DataOutputStream(bos);

然後在呼叫dos.writeXXX方法時效率就已經提高了。


BufferedInputStream & BufferedOutputStream

如上面所介紹的那樣,在BufferedInputStream的建構函式中需要傳入一個InputStream, BufferedInputStream內部有一個位元組陣列緩衝區,每次執行read操作的時候就從這buf中讀取資料,從buf中讀取資料沒有多大的開銷。如果buf中已經沒有了要讀取的資料,那麼就去執行其內部繫結的InputStream的read方法,而且是一次性讀取很大一塊資料,以便填充滿buf緩衝區。緩衝區buf的預設大小是8192位元組,也就是8K,在建構函式中我們也可以自己傳入一個size指定緩衝區的大小。由於我們在執行BufferedInputStream的read操作的時候,很多時候都是從緩衝區中讀取的資料,這樣就大大減少了實際執行其指定的InputStream的read操作的次數,也就提高了讀取的效率。與BufferedInputStream 相對的是BufferedOutputStream。在BufferedOutputStream的建構函式中我們需要傳入一個OutputStream,這樣就將BufferedOutputStream與該OutputStream繫結在了一起。BufferedOutputStream內部有一個位元組緩衝區buf,在執行write操作時,將要寫入的資料先一起快取在一起,將其存入位元組緩衝區buf中,buf是有限定大小的,預設的大小是8192位元組,即8KB,當然也可以在建構函式中傳入size指定buf的大小。該buf只要被指定了大小之後就不會自動擴容,所以其是有限定大小的,既然有限定大小,就會有被填充完的時刻,當buf被填充完畢的時候會呼叫BufferedOutputStream的flushBuffer方法,該方法會通過呼叫其繫結的OutputStream的write方法將buf中的資料進行實際的寫入操作並將buf的指向歸零(可以看做是將buf中的資料清空)。如果想讓快取區buf中的資料理解真的被寫入OutputStream中,可以呼叫flush方法,flush方法內部會呼叫flushBuffer方法。由於buf的存在,會大大減少實際執行OutputStream的write操作的次數,優化了寫的效率。
以下是BufferedInputStream 和 BufferedOutputStream的示例程式碼片段:

private static void testBufferedInputOutputStream(){
        try{
            String inputFileName = "D:\\iWork\\file1.txt";
            String outputFileName = "D:\\iWork\\file2.txt";
            FileInputStream fis = new FileInputStream(inputFileName);
            BufferedInputStream bis = new BufferedInputStream(fis, 1024 * 10);
            FileOutputStream fos = new FileOutputStream(outputFileName);
            BufferedOutputStream bos = new BufferedOutputStream(fos, 1024 * 10);
            byte[] buf = new byte[1024];
            int length = 0;
            while ((length = bis.read(buf)) > 0){
                bos.write(buf, 0, length);
            }
            bis.close();
            bos.close();
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

上面的程式碼將從file1.txt讀取檔案輸入流,然後將讀到的資料寫入到file2.txt中,即實現了將file1.txt拷貝到file2.txt中。其實不通過BufferedInputStream 和 BufferedOutputStream也可以完成這樣的工作,使用這個兩個類的好處是,可以對file1.txt的讀取以及file2.txt的寫入提高效率,從而提升檔案拷貝的效率。

相關文章