OpenJDK 源碼閱讀之 Java 字節流輸出類的實現

Java 的輸入輸出總是給人一種很混亂的感覺,要想把這個問題搞清楚,必須對各種與輸入輸出相關的類之間的關係有所瞭解。只有你瞭解了他們之間的關係,知道設計這個類的目的是什麼,才能更從容的使用他們。


這是這個系列的第二篇,描述字節輸出類的實現,第一篇見:OpenJDK 源碼閱讀之 Java 字節流輸入類的實現


字節流輸出

java_io_write_bytes

圖1 Java 字節輸出類

  • OutputStream

OutputStream是所有字節輸出類的超類,這是個抽象類,需要實現其中定義的 write 函數,纔能有實用的功能。

    public abstract void write(int b) throws IOException;

其它方法都是在 write 的基礎上實現的。例如這個多態的 write :

public void write(byte b[], int off, int len) 
throws IOException {
    if (b == null) {
        throw new NullPointerException();
    } else if ((off < 0) || (off > b.length) || (len < 0) ||
               ((off + len) > b.length) || ((off + len) < 0)) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return;
    }
    for (int i = 0 ; i < len ; i++) {
        write(b[off + i]);
    }
}
  • FileOutputStream

FileOutputStream 會將內容輸出到 File 或者 FileDescriptor, 此類是按照字節輸出,如果想按照字符輸出,可以使用FileReader 類。

構造器中,需要指明輸出的文件:

public FileOutputStream(File file, boolean append)
    throws FileNotFoundException
{
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkWrite(name);
    }
    if (name == null) {
        throw new NullPointerException();
    }
    this.fd = new FileDescriptor();
    this.append = append;

    fd.incrementAndGetUseCount();
    open(name, append);
}

寫入操作是一個 native 函數,與操作系統相關。

private native void write(int b, boolean append) throws IOException;

如果對比一下字節輸入類,你會發現輸入和輸出在實現上有很大的相似性,它們是對稱的。

  • ByteArrayOutputStream

ByteArrayOutputStream 會將數據寫入字節數組中, 可以通過 toByteArray,toString 得到這些數據。

protected byte buf[];

初始化時,可以指定這個數組的大小:

public ByteArrayOutputStream(int size) {
    if (size < 0) {
        throw new IllegalArgumentException("Negative initial size: "
                                           + size);
    }
    buf = new byte[size];
}

寫入時,會寫入這個數組。write 會先保證數組的大小,如果不夠用,還會自動進行擴充。

public synchronized void write(int b) {
    ensureCapacity(count + 1);
    buf[count] = (byte) b;
    count += 1;
}
  • FilterOutputStream

所有有過濾功能的類的基類,例如,對輸出流進行轉化,或者添加新的功能。初始化時,需要提供一個底層的流,用於寫入數據,FilterOUtputStream 類的所有方法都是通過調用這個底層流的方法實現的。

初始化時,

protected OutputStream out;
public FilterOutputStream(OutputStream out) {
    this.out = out;
}

寫入時:

public void write(int b) throws IOException {
    out.write(b);
}
  • BufferedOutputStream

BufferedOutputStream 是 FilterOutputStream 的子類,提供緩衝功能,所以,你不用每寫入一個字節都要調用操作系統的write 方法,而是積累到緩衝區,然後一起寫入。

緩衝區就是一個字節數組,在構造器中被初始化。

protected byte buf[];

public BufferedOutputStream(OutputStream out) {
    this(out, 8192);
}

public BufferedOutputStream(OutputStream out, int size) {
    super(out);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}

當調用 write(b) 時,並不真正寫入,而是將要寫入的數據存放在緩衝區內,等緩衝區滿後,一次性寫入數據。

public synchronized void write(int b) throws IOException {
    if (count >= buf.length) {
        flushBuffer();
    }
    buf[count++] = (byte)b;
}
  • DataOutputStream

DataOutputStream 可以按 Java 的基本類型寫入數據。寫入的原理是,將基本類型數據中的字節分離出來,然後將這些字節寫入。例如:

public final void writeBoolean(boolean v) throws IOException {
    out.write(v ? 1 : 0);
    incCount(1);
}

boolean 類型就是按照 0/1 的方式寫入的。

public final void writeShort(int v) throws IOException {
    out.write((v >>> 8) & 0xFF);
    out.write((v >>> 0) & 0xFF);
    incCount(2);
}

short 是兩個字節,需要將其中的兩個字節分離出來,分別寫入,incCount 加了2. writeChar 同理,因爲它也是寫入兩個字節。

浮點數比較特殊,沒法直接分離出各個字節,要調用 Float 的一個靜態方法,把浮點數轉化成四個字節,再通過 writeInt 寫入。floatToInitBits 會調用一個 native 方法, 按照 IEEE 754 標準,完成其主要功能。

    public final void writeFloat(float v) throws IOException {
        writeInt(Float.floatToIntBits(v));
    }
  • PipedOutputStream

管道輸出流可以與一個管道輸入流相關聯,關聯後,共用一個緩衝區,輸出流寫入數據,輸入流讀取數據,二者應該處於不同線程,否則可能出現死鎖。

原理上一篇文章在介紹 PipedInputStream 時,已經闡述。

另外,我覺得在這裏,有必要說一下那幾個用於壓縮和解壓縮的類,實現就不說了,就講下他們的功能與關係。

JAVA IO 壓縮與解壓縮

  • InflaterInputStream: 用於解壓 deflate 格式的壓縮數據,底層流爲壓縮後的數據,read 返回解壓後的數據。
  • InflaterOutputStream: 用於解壓 deflate 格式的壓縮數據,底層流爲壓縮後的數據,write 寫入解壓後的數據。
  • DeflaterInputStream: 用於壓縮成 deflate 格式的數據,底層流爲未壓縮數據,read 返回壓縮後的數據。
  • DeflaterOutputStream: 用於壓縮成 deflate 格式的數據,底層流爲未壓縮數據,write 寫入壓縮後的數據。

  • GZIPInputStream: 用於解壓 GZip 格式的壓縮數據,底層流爲壓縮後的數據,read 返回解壓後的數據。它是 InflaterInputStream 的子類。

  • GZIPOutputStream: 用於壓縮成 Gzip格式的數據,底層流爲未壓縮數據,write 寫入壓縮後的數據。是 DeflaterOutputStream 的子類(注意不是InflaterOutputStream) 。

不得不說,這個API設計的真是太反直覺了。GZIP 格式的解壓和壓縮一個是 GZIPInputStream,一個是 GZIPOutputStream。而 deflate 格式的解壓和壓縮,一個是 InflaterInputStream/InflaterOutputStream,另一個是 DeflaterInputStream/DeflaterOutputStream。當同時需要對 gzip 和 deflate 壓縮和解壓縮時,就感覺,真是反直覺。

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