Java編程拾遺『字節流』

上篇文章中簡單介紹了文件和Java IO的概念,我們瞭解到Java中文件是作爲一種特殊的IO設備處理的,並且Java中處理IO是通過流來操作的,流又可以細分爲字節流和字符流。本篇文章就重點介紹一下Java IO中的一個重要模塊——字節流。字節流的繼承體系如下圖所示:

本文重點介紹如下幾種字節流:

  • InputStream/OutputStream: 基類,抽象類。
  • ByteArrayInputStream/ByteArrayOutputStream: 輸入源和輸出目標是字節數組的流。
  • FileInputStream/FileOutputStream: 輸入源和輸出目標是文件的流。
  • FilterInputStream/FilterOutputStream,所有包裝流的父類,一些“特殊”功能的流,比如DataInputStream/DataOutputStream、BufferedInputStream/BufferedOutputStream都繼承了改類。
  • ObjectInputStream/ObjectOuputStream:輸入源和輸出目標是對象的流,用於實現Java序列化。

1. InputStream/OutputStream

InputStream和OutputStream是抽象類,是所有字節流的基類。

1.1 InputStream

S.N. 方法 說明
1 public abstract int read() throws IOException 從字節輸入流中讀取下一個字節,返回值爲讀取字節的int值
2 public int read(byte b[]) throws IOException 從字節輸入流中讀取多個字節,放入字節數組b中,返回值爲實際讀入的字節個數
3 public int read(byte b[], int off, int len) throws IOException 從字節輸入流中讀取len個字節放入字節數組b index off開始的位置, 返回值爲實際讀入的字節個數
4 public void close() throws IOException 關閉字節輸入流,釋放資源
5 public long skip(long n) throws IOException 跳過字節輸入流中n個字節,返回值爲實際跳過的字節個數
6 public int available() throws IOException 返回下一次不需要阻塞就能讀取到的字節個數,使用較少
7 public synchronized void mark(int readlimit) 標記能夠從字節輸入流中往後讀取的字節個數readlimit
8 public boolean markSupported() 判斷當前字節輸入流是否支持mark/reset操作
9 public synchronized void reset() throws IOException 重新從標記位置讀取字節輸入流
  • read()

read()方法會從字節輸入流中讀取下一個字節,返回類型爲int(一個字節8位,範圍0~255,所以int可以表示),當讀到流結尾的時候,返回值爲-1。如果字節輸入流中沒有數據,read方法會阻塞直到數據到來、流關閉、或異常出現。異常出現時,read方法拋出異常,類型爲IOException,這是一個受檢異常,調用者必須進行處理。read是一個抽象方法,具體子類必須實現。

  • read(byte b[])

read(byte b[])方法將從字節輸入流讀入的字節放入字節數組b中,第一個字節存入b[0],第二個存入b[1],以此類推,一次最多讀入的字節個數爲數組b的長度,但實際讀入的個數可能小於數組長度,返回值爲實際讀入的字節個數。如果剛開始讀取時已到流結尾,則返回-1,否則,只要數組長度大於0,該方法都會盡力至少讀取一個字節,如果流中一個字節都沒有,它也會阻塞直到數據到來、流關閉、或異常出現。該方法不是抽象方法,InputStream有一個默認實現,通過調用讀一個字節的read方法實現,但子類中一般會提供更爲高效的實現。

  • read(byte b[], int off, int len)

read(byte b[], int off, int len)方法會將讀入的第一個字節放入b[off],最多讀取len個字節。read(byte b[])方法就是調用這個方法實現的,如下:

public int read(byte b[]) throws IOException {
    return read(b, 0, b.length);
}
  • close()

close()方法作用是字節輸入流讀取結束後,關閉流,釋放相關資源。不管read方法是否拋出了異常,都應該調用close方法,所以close通常應該放在finally語句內。該方法不是抽象方法,InputStream中的實現是一個空函數,子類中會提供自己的實現。

  • skip()

skip()方法會跳過字節輸入流中n個字節,因爲輸入流中剩餘的字節個數可能不到n,所以返回值爲實際略過的字節個數。InputStream的默認實現就是調用readd()方法,盡力讀取n個字節,實現skip效果。子類往往會提供更爲高效的實現,比如在FileInputStream中會調用本地方法。在處理數據時,對於不感興趣的部分,skip往往比讀取然後扔掉的效率要高。

  • available()

available()方法返回下一次不需要阻塞就能讀取到的字節個數。InputStream的默認實現是返回0,子類會根據具體情況返回適當的值。在文件讀寫中,這個方法一般沒什麼用,但在從網絡讀取數據時,可以根據該方法的返回值在網絡有足夠數據時纔讀,以避免阻塞。

  • mark(int readlimit)

mark(int readlimit)方法用於標記當前字節輸入流讀取的位置,當字節輸入流已經讀取過該位置後,可以通過調用reset()方法,重新從標記位置讀取。readLimit,表示在設置了標記後,能夠繼續往後讀的最多字節數,如果超過了,標記會無效。因爲之所以能夠重讀,是因爲流能夠將從標記位置開始的字節保存起來,而保存消耗的內存不能無限大,流只保證不會小於readLimit。在InputStream中該方法是個空方法,具體子類要根據自身情況覆蓋該方法。

  • markSupported()

markSupported()方法用於獲取當前字節輸入流是否支持mark/reset操作,返回true表示當前字節輸入流支持mark/reset操作。InpuStream類中的默認實現是不支持,子類根據自身情況覆蓋該方法。

  • reset()

reset()方法用於重新從mark標記位置讀取字節輸入流。

1.2 OutputStream

S.N. 方法 說明
1 public abstract void write(int b) throws IOException 向字節輸出流中寫入一個字節
2 public void write(byte b[]) throws IOException 將字節數組b中所有的字節寫入字節輸出流
3 public void write(byte b[], int off, int len) throws IOException 將字節數組b中從index off開始,長度爲len的字節寫入字節輸出流
4 public void flush() throws IOException 將緩衝而未實際寫的數據進行實際寫入
5 public void close() throws IOException 關閉字節輸出流,釋放資源
  • write(int b)

write(int b)方法向流中寫入一個字節,參數類型雖然是int,但其實只會用到最低的8位(一個字節)。該方法在OutputStream中是個抽象方法,子類需要根據自身情況實現該方法。

  • write(byte b[], int off, int len)

write(byte b[], int off, int len)方法會將字節數組中index從off開始的len個字節寫入到字節輸出流中,第一個寫入的字節是b[off],寫入個數爲len,最後一個是b[off+len-1]。OutputStream的默認實現是循環調用單字節的write方法,子類往往有更爲高效的實現。

  • write(byte b[])

write(byte b[])方法會將字節數組中的全部字節寫入到字節輸出流中,在OutputStream中的實現是write(b, 0, b.length)。

  • flush()

flush()方法會將緩衝而未實際寫的數據進行實際寫入。

  • close()

close()方法會關閉字節輸出流,釋放系統資源。

2. ByteArrayInputStream/ByteArrayOutputStream

2.1 ByteArrayInputStream

ByteArrayInputStream的作用是將byte數組包裝爲一個輸入流(適配器模式 )。ByteArrayInputStream可以通過如下兩種方式構建:

public ByteArrayInputStream(byte buf[]);
public ByteArrayInputStream(byte buf[], int offset, int length);

第一個構造函數是將字節數組buf中的全部字節包裝到ByteArrayInputStream(效果是buf數組中的全部字節都體現在ByteArrayInputStream中)。第二個構造函數是將字節數組buf中index從offset開始長度爲length的所有字節包裝到ByteArrayInputStream(效果是buf數組中offset到offset + len -1的字節體現在ByteArrayInputStream中,read操作也只能讀取到offset及之後的字節)。ByteArrayInputStream的所有數據都在內存,支持mark/reset重複讀取。

2.2 ByteArrayOutputStream

ByteArrayOutputStream的作用是將內存中的數據輸出到一個byte數組中,充當這個輸出操作的“管道”。ByteArrayOutputStream可以通過如下兩種方式構建:

public ByteArrayOutputStream();
public ByteArrayOutputStream(int size);

第二個構造函數中的size是用來存儲內存中數據的字節數組的大小。第一個構造函數的實現是通過調用第二個構造函數實現的,size默認爲32。在調用write方法向ByteArrayOutputStream流中寫值的過程中,如果字節數組長度不夠,會進行動態擴容,擴展後的容量是擴容前容量的兩倍。

S.N. 方法 說明
1 public synchronized byte[] toByteArray() 將ByteArrayOutputStream流中的內容輸出到字節數組
2 public synchronized String toString() 以系統默認編碼,將ByteArrayOutputStream流中的內容(write操作寫進來字節數組)輸出爲字符串
3 public synchronized String toString(String charsetName) 使用特定編碼,將ByteArrayOutputStream流中的內容(write操作寫進來字節數組)輸出爲字符串
4 public synchronized void writeTo(OutputStream out) throws IOException 將ByteArrayOutputStream中的數據寫到另一個OutputStream中
5 public synchronized int size() 返回當前已經寫入的字節個數
6 public synchronized void reset() 重置ByteArrayOutputStream中的內容,之前寫入的內容都會無效

2.3 使用示例

@SneakyThrows
public static void main(String[] args) {
    byte[] bytes = Bytes.toArray(Lists.newArrayList(65, 66, 67, 68));

    try (InputStream inputStream = new ByteArrayInputStream(bytes);
         ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        writeStream(inputStream, output);
    }

    try (InputStream inputStream = new ByteArrayInputStream(bytes, 1, 2);
         ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        writeStream(inputStream, output);
    }
}

private static void writeStream(InputStream inputStream, ByteArrayOutputStream output) throws IOException {
    byte[] buf = new byte[16];
    int bytesRead;
    while ((bytesRead = inputStream.read(buf)) != -1) {
        output.write(buf, 0, bytesRead);
    }
    String data = output.toString("UTF-8");
    System.out.println(data);
}

輸出結果:

ABCD
BC

關於上面這段代碼,簡單講述一下:

  • Bytes.toArray()方法是google Guava工具包提供的方法,代碼中的作用是將List<Integer>轉化爲一個byte數組,詳見之前的一篇文章Guava元語工具
  • try () {}這種格式是Java8提供的語法糖try-with-resource,可以在try代碼塊執行結束,自動關閉打開的資源
  • 兩個try代碼塊,分別測試了ByteArrayInputStream的兩種構造函數,根據運行結果,可以發現是符合預期的,第一個ByteArrayInputStream對象通過整個byte數組構造,第二個ByteArrayInputStream對象通過byte數組index 1開始到index 2結束的字節構建
  • output.write操作將從ByteArrayInputStream中讀取的字節數組寫入到ByteArrayOutputStream對象output中
  • output.toString(“UTF-8”)使用UTF-8編碼將ByteArrayOutputStream中的內容輸出爲字符串

3. FileInputStream/FileOutputStream

3.1 FileInputStream

FileInputStream是文件和內存之間交互的“水管”,是一種將文件讀入到內存中的工具流。可以通過如下兩種方式構造:

public FileInputStream(String name) throws FileNotFoundException
public FileInputStream(File file) throws FileNotFoundException

其中第一個構造函數的參數是文件路徑,第二個構造函數的參數是文件對象。其中文件路徑和文件必須是一個存在的文件,並且不能是目錄,否則會拋FileNotFoundException。下面通過一個簡單的例子看一下FileInputStream的用法:

@SneakyThrows
public static void main(String[] args) {
    try (InputStream input = new FileInputStream("d:/hello.txt")) {
        byte[] buf = new byte[1024];
        int b;
        int bytesRead = 0;
        while ((b = input.read()) != -1) {
            buf[bytesRead++] = (byte) b;
        }
        String data = new String(buf, 0, bytesRead, StandardCharsets.UTF_8);
        System.out.println(data);
    }

    try (InputStream input = new FileInputStream(new File("d:/hello.txt"))) {
        byte[] buf = new byte[1024];
        int bytesRead = input.read(buf);
        String data = new String(buf, 0, bytesRead, StandardCharsets.UTF_8);
        System.out.println(data);
    }

    try (InputStream input = new FileInputStream(new File("d:/hello.txt"));
         ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        byte[] buf = new byte[1024];
        int bytesRead;
        while ((bytesRead = input.read(buf)) != -1) {
            output.write(buf, 0, bytesRead);
        }
        String data = output.toString("UTF-8");
        System.out.println(data);
    }
}

代碼邏輯很簡單,作用就是通過FileInputStream讀取文件,然後將文件內容作爲String打印輸出。上述三個try代碼塊分別表示三種實現方式。

  • 第一種方式通過逐個字節讀取的方式,將FileInputStream流中的內容讀取到字節數組buf中,這種方式沒有緩存,逐個字節讀取,效率較差,且建立在文件內容小於1024個字節的前提下。最後通過String的構造函數,將buf數組轉化爲String。
  • 第二種方式通過FileInputStream的一次read操作,將FileInputStream流中的內容讀取到字節數組buf中,這種方式較第一種方式效率上會好一些,但是依然建立在文件內容小於1024個字節的前提下。最後通過String的構造函數,將buf數組轉化爲String。
  • 第三種方式藉助ByteArrayOutputStream,每次從FileInputSteam流中讀取1024個字節到字節數組buf中,然後再將字節數組內容寫入到ByteArrayOutputStream中,由於ByteArrayOutputStream支持動態擴容,不用像之前兩種方式那樣,依賴於文件內容的大小,所以這種方式是一種比較合理的方式。

3.2 FileOutputStream

FileOutputStream的作用是充當內存到文件之間的“管道”,可以方便地將內存中的數據寫入到文件中,可以通過如下幾種方式構造:

public FileOutputStream(File file) throws FileNotFoundException
public FileOutputStream(File file, boolean append) throws FileNotFoundException
public FileOutputStream(String name) throws FileNotFoundException
public FileOutputStream(String name, boolean append) throws FileNotFoundException

可以通過兩種方式構造FileOutPutStream對象,一種是File對象(必須是文件,不是文件路徑),另一種是文件路徑(路徑可以是絕對路徑,也可以是相對路徑)。如果文件已存在,參數append指定是追加還是覆蓋,true表示追加,false表示覆蓋(沒有append參數地構造函數,append默認爲false,表示覆蓋文件)。下面通過一個簡單地例子來看一下FileOutPutStream地用法:

@SneakyThrows
public static void main(String[] args) {
    try (OutputStream output = new FileOutputStream(new File("d:/hello1.txt"))) {
        String str = "卓立123";
        byte[] bytes = str.getBytes(Charset.forName("UTF-8"));
        output.write(bytes);
    }
}

首先通過getBytes方法將內存中的字符串轉化爲byte數組,然後調用FileOutPutStream對象的write方法,將byte數組的內容寫入到文件中,比較簡單,這裏就不多講了。

3. FilterInputStream/FilterOutputStream

上面講的流,基本的流按字節讀寫,沒有緩衝區,這不方便使用,Java解決這個問題的方法是使用裝飾器設計模式。FilterInputStream/FilterOutputStream是所有裝飾流的父類,通過名稱也能大概猜出FilterInputStream/FilterOutputStream是幹嘛的,就是過濾的意思,類似於自來水管道,流入的是水,流出的也是水,功能不變,或者只是增加功能,它有很多子類:

  • DataInputStream/DataOutputStream:按八種基本類型和字符串對流進行讀寫
  • BufferedInputStream/BufferedOutputStream:對基礎流進行緩衝讀寫

3.1 DataInputStream/DataOutputStream

3.1.1 DataInputStream

DataInputStream是裝飾類FilterInputStream的子類,可以方便地對被包裝流中的數據通過基礎數據類型讀取。由於DataInputStream的作用是對其它字節輸入流做包裝,所以構造函數中肯定事要傳入被包裝流的對象的,JDK實現也是這樣,如下:

public DataInputStream(InputStream in)

除了基礎的read方法外,DataInputStream中也提供了針對基本數據類型的讀取方法,如下:

S.N. 方法 說明
1 public final int read(byte b[]) throws IOException 從被包裝字節輸入流中讀取多個字節,放入字節數組b中,返回值爲實際讀入的字節個數
2 public final int read(byte b[], int off, int len) throws IOException 從被包裝字節輸入流中讀取len個字節放入字節數組b index off開始的位置, 返回值爲實際讀入的字節個數
3 public final boolean readBoolean() throws IOException 從被包裝字節輸入流中讀取一個字節,並轉化爲boolean輸出
4 public final byte readByte() throws IOException 從被包裝字節輸入流中讀取一個字節輸出
5 public final char readChar() throws IOException 從被包裝字節輸入流中讀取兩個字節,並轉化爲char輸出
6 public final double readDouble() throws IOException 從被包裝字節輸入流中讀取八個字節,並轉化爲double輸出
7 public final float readFloat() throws IOException 從被包裝字節輸入流中讀取四個字節,並轉化爲float輸出
8 public final int readInt() throws IOException 從被包裝字節輸入流中讀取四個字節,並轉化爲int輸出
9 public final long readLong() throws IOException 從被包裝字節輸入流中讀取八個字節,並轉化爲long輸出
10 public final short readShort() throws IOException 從被包裝字節輸入流中讀取兩個字節,並轉化爲short輸出
11 public final int readUnsignedByte() throws IOException 從被包裝字節輸入流中讀取一個字節,以無符號byte輸出
12 public final int readUnsignedShort() throws IOException 從被包裝字節輸入流中讀取兩個字節,並轉化爲無符號short輸出
13 public final String readUTF() throws IOException 讀取在已使用UTF-8修改版格式編碼的字符串(就是使用DataOutputStream的writeUTF寫入的)

3.1.2 DataOutputStream

DataOutputStream是裝飾類FilterOutputStream的子類,可以方便地將內存中的數據通過基礎數據類型寫入到被包裝流的輸出目標中。比如被包裝流是FileOutputStream對象,那麼調用dataOutputSream的writeInt操作,就可以將一個int值寫入到FileOutputStream對象的目標文件中。DataOutputStream可以通過如下方式構建:

public DataOutputStream(OutputStream out)

除了構造函數,DataOutputStream提供如下寫操作:

S.N. 方法 說明
1 public synchronized void write(int b) throws IOException 寫入一個字節
2 public synchronized void write(byte b[], int off, int len)  
3 public final void writeBoolean(boolean v) throws IOException 寫入一個字節,如果值爲true,則寫入1,否則0
4 public final void writeByte(int v) throws IOException  
5 public final void writeBytes(String s) throws IOException  
6 public final void writeChar(int v) throws IOException  
7 public final void writeChars(String s) throws IOException  
8 public final void writeDouble(double v) throws IOException  
9 public final void writeFloat(float v) throws IOException  
10 public final void writeInt(int v) throws IOException 寫入四個字節,最高位字節先寫入,最低位最後寫入
11 public final void writeLong(long v) throws IOException  
12 public final void writeShort(int v) throws IOException  
13 public final void writeUTF(String str) throws IOException 將字符串的UTF-8編碼字節寫入,這個編碼格式與標準的UTF-8編碼略有不同

3.1.3 示例

定義一個簡單實體類Student:

@Getter
@Setter
@AllArgsConstructor
@ToString
public class Student {
    private String name;
    private int age;
    private float grade;
}

通過DataInputStream和DataOutputStream進行讀寫如下:

@SneakyThrows
public static void main(String[] args) {

    try (OutputStream fileOutputStream = new FileOutputStream("d:/hello.txt");
         DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream)
    ) {
        List<Student> listStudent = new ArrayList<>();

        listStudent.add(new Student("Alice", 23, 80.5f));
        listStudent.add(new Student("Brian", 22, 95.0f));
        listStudent.add(new Student("Carol", 21, 79.8f));

        for (Student student : listStudent) {
            dataOutputStream.writeUTF(student.getName());
            dataOutputStream.writeInt(student.getAge());
            dataOutputStream.writeFloat(student.getGrade());
        }
    }


    try (InputStream fileInputStream = new FileInputStream("d:/hello.txt");
         DataInputStream dataInputStream = new DataInputStream(fileInputStream)
    ) {
        List<Student> listStudent = new ArrayList<>();
        try {
            while (true) {
                String name = dataInputStream.readUTF();
                int age = dataInputStream.readInt();
                float grade = dataInputStream.readFloat();

                Student student = new Student(name, age, grade);
                listStudent.add(student);
            }
        } catch (EOFException ex) {
            //流讀取完畢
        }

        System.out.println(listStudent);
    }
}

運行結果:

[Student(name=Alice, age=23, grade=80.5), Student(name=Brian, age=22, grade=95.0), Student(name=Carol, age=21, grade=79.8)]

使用DataInputStream/DataOutputStream讀寫對象,非常靈活,但比較麻煩,所以Java提供了序列化機制ObjectInputSream/ObjectOutputStream。

3.2 BuffedInputStream/BuffedOutputStream

3.2.1 BuffedInputStream

BuffedInputStream是裝飾類FilterInputStream的子類,提供普通流讀取的緩衝功能,舉個例子,如果普通的流是個水管,那麼BuffedInputStream就是一個蓄水池。BufferedInputStream內部有個字節數組作爲緩衝區,讀取時,先從這個緩衝區讀,緩衝區讀完了再調用包裝的流讀,可以通過如下兩種方式構造:

public BufferedInputStream(InputStream in)
public BufferedInputStream(InputStream in, int size)

InputStream表示被包裝流,第二個構造函數中的size表示用來緩衝的數組的大小。如果沒指定size,那麼緩衝數組長度默認爲8192。

除了構造方法之外,BuffedInputStream重寫了InputStream的read方法,沒有提供其他自定義方法。

S.N. 方法 說明
1 public synchronized int read() throws IOException 從BuffedInputStream中讀取下一個字節,返回值爲讀取字節的int值
3 public synchronized int read(byte b[], int off, int len) throws IOException 從BuffedInputStream中讀取len個字節放入字節數組b index off開始的位置, 返回值爲實際讀入的字節個數
4 public void close() throws IOException 關閉BuffedInputStream,釋放資源
5 public long skip(long n) throws IOException 跳過BuffedInputStream中n個字節,返回值爲實際跳過的字節個數
6 public synchronized int available() throws IOException 返回下一次不需要阻塞就能讀取到的字節個數,使用較少
7 public synchronized void mark(int readlimit) 標記能夠從字節輸入流中往後讀取的字節個數readlimit
8 public boolean markSupported() 默認返回true,BufferedInputSream支持mark/reset操作
9 public synchronized void reset() throws IOException 重新從標記位置讀取字節輸入流

3.2.1 BuffedOutputStream

BufferedOutputStream是裝飾類FilterOutputStream的子類,提供普通流讀取的緩衝功能,同BufferedInputStream類似,BufferedOutputStream也可以通過如下兩種構建:

public BufferedOutputStream(OutputStream out)
public BufferedOutputStream(OutputStream out, int size)
S.N. 方法 說明
1 public synchronized void write(int b) throws IOException 向BufferedOutputStream中寫入一個字節
3 public synchronized void write(byte b[], int off, int len) throws IOException 將字節數組b中從index off開始,長度爲len的字節寫入BufferedOutputStream
4 public void flush() throws IOException 將緩衝而未實際寫的數據進行實際寫入

3.2.3 示例

@SneakyThrows
public static void main(String[] args) {
    try (OutputStream fileOutputStream = new FileOutputStream("d:/hello.txt");
         BufferedOutputStream bufferedInputStream = new BufferedOutputStream(fileOutputStream);
         DataOutputStream dataOutputStream = new DataOutputStream(bufferedInputStream)
    ) {
        List<Student> listStudent = new ArrayList<>();

        listStudent.add(new Student("Alice", 23, 80.5f));
        listStudent.add(new Student("Brian", 22, 95.0f));
        listStudent.add(new Student("Carol", 21, 79.8f));

        for (Student student : listStudent) {
            dataOutputStream.writeUTF(student.getName());
            dataOutputStream.writeInt(student.getAge());
            dataOutputStream.writeFloat(student.getGrade());
        }
    }


    try (InputStream fileInputStream = new FileInputStream("d:/hello.txt");
         BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
         DataInputStream dataInputStream = new DataInputStream(bufferedInputStream)
    ) {
        List<Student> listStudent = new ArrayList<>();
        try {
            while (true) {
                String name = dataInputStream.readUTF();
                int age = dataInputStream.readInt();
                float grade = dataInputStream.readFloat();

                Student student = new Student(name, age, grade);
                listStudent.add(student);
            }
        } catch (EOFException ex) {
            //流讀取完畢
        }

        System.out.println(listStudent);
    }
}

跟上節示例目標一致,通過DataOutputStream向文件中寫入對象,然後通過DataInputStream從文件中讀取對象。但是上節的示例中,讀寫文件都是直接讀寫的,比如readInt操作,就要一個字節一個字節的從FileInputStream中讀取內容,效率時不高的。如果像本節示例中,通過BufferedInputStream將FileInputStream對象包裝一下,readInt操作就可以從緩衝中讀取內容了,BufferedOutputStream操作同樣的道理。

4. ObjectInputStream/ObjectOuputStream

ObjectInputStream/ObjectOuputStream是java中用來實現序列化和反序列化默認機制的,從IO流的角度來講,ObjectInputStream/ObjectOuputStream其實就是源和目標是對象的IO輸入輸出流。

4.1 ObjectInputStream

ObjectInputStream可以用來實現反序列化,可以通過如下方式構建:

public ObjectInputStream(InputStream in) throws IOException

之前講過DataInputStream可以按照基本數據類型讀取流中的內容,比較靈活,但是比較麻煩。相比於DataInputStream,ObjectInputSream也提供了基本數據類型的讀取方法,除此之外,ObjectInputSream還提供了readObject方法,ObjectOutputStream通過writeObject方法寫入流中的對象。

S.N. 方法 說明
1 public int read() throws IOException 從ObjectInputSream中讀取一個字節
2 public int read(byte b[], int off, int len) throws IOException 從ObjectInputSream中讀取len個字節放入字節數組b index off開始的位置, 返回值爲實際讀入的字節個數
3 public boolean readBoolean() throws IOException 從ObjectInputSream中讀取一個字節,並轉化爲boolean輸出
4 public byte readByte() throws IOException 從ObjectInputSream中讀取一個字節輸出
5 public char readChar() throws IOException 從ObjectInputSream中讀取兩個字節,並轉化爲char輸出
6 public double readDouble() throws IOException 從ObjectInputSream中讀取八個字節,並轉化爲double輸出
7 public float readFloat() throws IOException 從ObjectInputSream中讀取四個字節,並轉化爲float輸出
8 public int readInt() throws IOException 從ObjectInputSream中讀取四個字節,並轉化爲int輸出
9 public long readLong() throws IOException 從ObjectInputSream中讀取八個字節,並轉化爲long輸出
10 public final Object readObject() throws IOException, ClassNotFoundException 反序列化,從ObjectInputSream讀取序列化的對象
11 public short readShort() throws IOException 從ObjectInputSream中讀取兩個字節,並轉化爲short輸出
12 public int readUnsignedByte() throws IOException 從ObjectInputSream中讀取一個字節,以無符號byte輸出
13 public int readUnsignedShort() throws IOException 從ObjectInputSream中讀取兩個字節,並轉化爲無符號short輸出
14 public String readUTF() throws IOException 從ObjectInputSream中讀取在已使用UTF-8修改版格式編碼的字符串(就是使用ObjectOutputSream的writeUTF寫入的)

4.2 ObjectOutputStream

ObjectOutputStream是源爲對象的字節輸出流,主要用來將對象序列化,可以通過如下方式構建:

public ObjectOutputStream(OutputStream out) throws IOException

與DataOutputStream類似,ObjectOutputStream也提供了基礎類型數據寫入字節流的方法,除此之外主要提供了用來實現對象序列化的writeObject方法

S.N. 方法 說明
1 public void write(int val) throws IOException 寫入一個字節
2 public void write(byte[] buf) throws IOException  
2 public void write(byte[] buf, int off, int len) throws IOException  
  private void writeArray(Object array, ObjectStreamClass desc, boolean unshared)  
3 public void writeBoolean(boolean val) throws IOException 寫入一個字節,如果值爲true,則寫入1,否則0
4 public void writeByte(int val) throws IOException  
5 public void writeBytes(String str) throws IOException  
6 public void writeChar(int val) throws IOException  
7 public void writeChars(String str) throws IOException  
8 public void writeDouble(double val) throws IOException  
9 public void writeFloat(float val) throws IOException  
10 public void writeInt(int val) throws IOException 寫入四個字節,最高位字節先寫入,最低位最後寫入
11 public void writeLong(long val) throws IOException  
12 public final void writeObject(Object obj) throws IOException  
13 public void writeShort(int val) throws IOException  
14 public void writeUTF(String str) throws IOException 將字符串的UTF-8編碼字節寫入,這個編碼格式與標準的UTF-8編碼略有不同

4.3 示例

@SneakyThrows
public static void main(String[] args) {
    try (OutputStream fileOutputStream = new FileOutputStream("d:/student.dat");
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {

        Student student = new Student("Michael", 23, 1);
        objectOutputStream.writeObject(student);
    }

    try (InputStream fileInputStream = new FileInputStream("d:/student.dat");
         ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
        Student student = (Student) objectInputStream.readObject();
        System.out.println(student);
    }
}

通過ObjectOutputStream像對象的序列化結果寫入到文件中,然後通過ObjectInputStream反序列化,運行結果:

Student(name=Michael, age=23, grade=1.0)

按照之前講的字符文件和二進制文件的定義,上述序列化得到的文件就是二進制文件了(可以記事本打開看一下,會亂碼,因爲除了對象內容外,還有很多控制信息)。對象序列化就是將對象按照某種規則轉爲另一種形式(比如字節流,字符串),以方便存儲,傳播。但序列化則是序列化的逆過程,按照約定的規則解析序列化結果,得到序列化前的對象。除了Java默認提供的ObjectInputStream/ObjectOuputStream可以實現序列化/反序列化之外,還有很多工具可以同樣可以實現,並且使用起來時間效率和空間效率要比ObjectInputStream/ObjectOuputStream要好,比如Hession、FastJson、Gson、Protobuf等,特別是谷歌提供的Protobuf,在近年來應用很廣泛,這裏不繼續介紹了,之後打算專門寫一篇關於Protobuf的文章介紹一下,有興趣的同學也可以去了解一下:Google Protocol Buffers

參考鏈接:

1. JDK源碼

2. 《Java編程的邏輯 》

發佈了117 篇原創文章 · 獲贊 33 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章