天天看點

java 位元組輸入流/輸出流先序1、Bits 工具類 2、輸入流 InputStream3、輸出流 OutputStream

目錄

先序

1、Bits 工具類

 2、輸入流 InputStream

2.1、位元組數組輸入流 ByteArrayInputStream

2.2、過濾輸入流 FilterInputStream 

2.2.1、緩沖輸入流 BufferedInputStream

2.2.2、 資料輸入流 DataInputStream

2.2.3、回推輸入流 PushbackInputStream

2.3、檔案輸入流 FileInputStream

3、輸出流 OutputStream

3.1、位元組數組輸出流 ByteArrayOutputStream

3.2、過濾輸出流 FilterOutputStream

3.2.1、緩沖輸出流 BufferedOutputStream

3.2.2、DataOutputStream 資料輸出流

3.3、檔案輸出流 FileOutputStream

先序

下面這張圖是來自這篇部落格https://blog.csdn.net/u010145219/article/details/89792877,我覺得梳理得很棒,是以借用一下。

java 位元組輸入流/輸出流先序1、Bits 工具類 2、輸入流 InputStream3、輸出流 OutputStream

1、Bits 工具類

Bits 是一個工具類,一般是供 java io 包下的類使用的,用于處理位元組,因為輸入輸出流分為 位元組流 和 字元流。接下來看看有哪些方法:

1、static boolean getBoolean(byte[] b, int off):判斷位元組數組 b 的第 off 位置是否不為0,true表示不為0。

2、static char getChar(byte[] b, int off):從 off 開始,擷取位元組數組 b 的一個字元,因為 java 中一個 char 代表2個位元組,因為會将 b 數組的 off 和 off + 1 位置的2個位元組拼成一個字元。

3、static short getShort(byte[] b, int off):short 也是2個位元組,是以也是将 b 數組的 off 和 off + 1 位置拼成一個short。

4、static int getInt(byte[] b, int off):int 是4位元組,是以将 b 數組的 off、off + 1、off + 2 和 off + 3 位置拼成一個int。

5、static float getFloat(byte[] b, int off):float 是4位元組,将 b 數組的 off 至 off + 3 位置先拼成一個int,再利用 Float 類的靜态本地方法 intBitsToFloat(int a) 轉為 float。

6、static long getLong(byte[] b, int off):long 是8位元組,将 b 數組的 off 至 off + 7 位置拼成一個long。

7、static double getDouble(byte[] b, int off):double 是8位元組,先将 b 數組的 off 至 off + 7 位置拼成一個long,再利用Double類的靜态本地方法 longBitsToDouble(long a) 轉為 double。

8、static void putBoolean(byte[] b, int off, boolean val):将 boolean 值轉換為位元組,存儲于數組 b 的 off 位置上。

9、static void putChar(byte[] b, int off, char val):将 char 值分為2個位元組,分别存于數組 b 的off 和 off + 1 位置上。

10、static void putShort(byte[] b, int off, short val):将 short 值分為2個位元組,分别存于數組 b 的off 和 off + 1 位置上。

11、static void putInt(byte[] b, int off, int val):将 int 值分為4個位元組,分别存于數組 b 的 off 至 off + 3 位置上。

12、static void putFloat(byte[] b, int off, float val):先用 Float 類的靜态本地方法 floatToIntBits(float a) 将 float 轉為 int,再将 int 值分為4個位元組,存儲于數組 b 裡面。

13、static void putLong(byte[] b, int off, long val):将 long 值分為8個位元組,分别存于數組 b 的 off 至 off + 7 位置上。

14、static void putDouble(byte[] b, int off, double val):先用 Double 類的靜态本地方法 doubleToLongBits(double a) 将double 轉為 long,再将 long 存儲于數組 b 中。

我覺得重要的不是學習這些方法的功能,而是學習這些方法的源代碼,如果進行位元組的處理與轉換的,下面看4個方法:

static boolean getBoolean(byte[] b, int off) {
        return b[off] != 0;
    }

    static char getChar(byte[] b, int off) {
        return (char) ((b[off + 1] & 0xFF) +
                       (b[off] << 8));
    }

    static void putBoolean(byte[] b, int off, boolean val) {
        b[off] = (byte) (val ? 1 : 0);
    }

    static void putChar(byte[] b, int off, char val) {
        b[off + 1] = (byte) (val      );
        b[off    ] = (byte) (val >>> 8);
    }
           

 2、輸入流 InputStream

讀取位元組流的話,都是基于read() 無參方法的,但是read() 的具體實作要看具體的輸入流子類。InputStream 抽象類定義了輸入流的一些規範和操作。

public abstract class InputStream implements Closeable {

    // 能夠跳過的最大位元組數
    private static final int MAX_SKIP_BUFFER_SIZE = 2048;

    // 讀取輸入流中下一個位元組,傳回讀取到的位元組,如果輸入流到底了,就傳回 - 1。
    public abstract int read() throws IOException;

    // 讀取輸入流中的位元組,如果足夠的話,把數組b裝滿,傳回實際讀取的位元組數
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    //讀取輸入流中第off位置的位元組開始,讀取len個位元組,存儲到位元組數組 b 中,傳回實際讀取的位元組數
    public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read();
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }

    // 從輸入流的目前位置往後跳 n 個位元組,傳回的是實際跳了多少位元組,因為也許輸入流中剩下的位元組數
    // 小于n,意味着跳過的位元組就無法擷取了,除非用mark 和 reset
    public long skip(long n) throws IOException {

        long remaining = n;
        int nr;

        if (n <= 0) {
            return 0;
        }

        int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
        byte[] skipBuffer = new byte[size];
        while (remaining > 0) {
            nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
            if (nr < 0) {
                break;
            }
            remaining -= nr;
        }

        return n - remaining;
    }

    // 傳回下一次從此輸入流中可以不阻塞地讀取多少位元組的資料,不同的輸入流類有不同的實作,有些是
    // 傳回輸入的位元組總數
    public int available() throws IOException {
        return 0;
    }

    // 關閉輸入流方法
    public void close() throws IOException {}

    // mark方法,稍後詳解介紹
    public synchronized void mark(int readlimit) {}

    // reset方法,稍後詳細介紹
    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }

    // 判斷是否支援 mark() 和 reset() 功能
    public boolean markSupported() {
        return false;
    }

}
           

2.1、位元組數組輸入流 ByteArrayInputStream

ByteArrayInputStream 的作用:将一個位元組數組進行封裝,封裝為 ByteArrayInputStream,封裝的好處呢?ByteArrayInputStream 提供了一套便于操作位元組數組的接口,如果我們直接對位元組數組進行操作的話,非常麻煩,要寫不少代碼,而且代碼難以複用,如果封裝為 ByteArrayInputStream 的話,可以更加靈活地操作位元組數組。

基本原理:對于一個目标位元組數組,ByteArrayInputStream 内部有一個位元組數組 buf,專門用于存儲目标位元組數組,僅僅是淺拷貝,沒有建立新的數組空間,對于數組 buf, 有2個重要的屬性:pos 和 count, count 是數組裡的位元組總數,初始化好了之後就不會改變了,pos 是指向 buf 裡下一次将要讀取的位元組,是以每讀取一個位元組,pos 就往 buf 尾部移動一個位置。通過 pos 和 count 的比較可以判斷出是否已經讀取完了。 

下面是 ByteArrayInputStream 的源碼,每個方法都有注釋,這裡特别說一下 mark 和 reset 方法,mark 方法是一個變量,用于存儲目前 pos 的值,也就是将目前的 pos 這個位置存起來,存起來幹嘛呢?便于随時将 pos 指回 mark 存的這個位置,因為沒有方法能夠讓 pos 往 buf 數組頭方向移動,是以一旦讀取過的位元組,将不再有機會讀取了,但是 mark 就可以讓 pos 再回到之前的某個位置,重新開始讀取。 mark 隻是存儲目前 pos 的位置,隻有調用了 reset 方法,才能使 mark 這個位置起作用。

public class ByteArrayInputStream extends InputStream {
    // 存儲讀取到的位元組
    protected byte buf[];
    // 将要從輸入流中讀取的下一個位元組的位置序号
    protected int pos;
    // mark 标記位置,預設為 0 
    protected int mark = 0;
    // buf數組中位元組的個數
    protected int count;
    // 構造方法,帶有初始化位元組數組
    public ByteArrayInputStream(byte buf[]) {
        this.buf = buf;
        this.pos = 0;
        this.count = buf.length;
    }
    // 構造方法,将參數 buf 數組的offset開始的length個位元組(不包含offset)存于輸入流的buf數
//組,如果offset + length超出了參數buf數組,就将buf數組從offset開始的剩餘元素全部存于輸入流的
//buf數組。
    public ByteArrayInputStream(byte buf[], int offset, int length) {
        this.buf = buf;
        this.pos = offset;
        this.count = Math.min(offset + length, buf.length);
        this.mark = offset;
    }
    // 從此輸入流的buf數組中讀取下一個位元組,如果到底了,就傳回 -1
    public synchronized int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }
    // 從此輸入流的buf數組中讀取off開始的len個元素存儲于b數組
    public synchronized int read(byte b[], int off, int len) {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        }
        if (pos >= count) {
            return -1;
        }
        int avail = count - pos;
        if (len > avail) {
            len = avail;
        }
        if (len <= 0) {
            return 0;
        }
        System.arraycopy(buf, pos, b, off, len);
        pos += len;
        return len;
    }
    // 跳過 n 個位元組
    public synchronized long skip(long n) {
        long k = count - pos;
        if (n < k) {
            k = n < 0 ? 0 : n;
        }
        pos += k;
        return k;
    }
    // 傳回此輸入流中還有多少個位元組沒有被讀取
    public synchronized int available() {
        return count - pos;
    }
    // 表示支援mark 和 reset 功能
    public boolean markSupported() {
        return true;
    }
    // 将下一個要讀取的位元組的位置标記起來
    public void mark(int readAheadLimit) {
        mark = pos;
    }
    // 重置,把将要讀取的下一個位元組的位置改為mark标記的位置
    public synchronized void reset() {
        pos = mark;
    }
    // 關閉沒有任何操作,是以ByteArrayInputStream 輸入流不需要關閉,java虛拟機自管
    public void close() throws IOException {}
}
           

2.2、過濾輸入流 FilterInputStream 

下面是 FilterInputStream 的源碼,可以看到,就是進行了一層封裝,什麼都沒有,那它這麼做有什麼用呢?其實,這就是一種經典的設計模式——裝飾者模式,FilterInputStream 類不能被我們隻用使用,因為沒用,但是确實是裝飾者模式實作的基石。

public class FilterInputStream extends InputStream {
    protected volatile InputStream in;
    protected FilterInputStream(InputStream in) {     this.in = in;    }
    public int read() throws IOException {     return in.read();    }
    public int read(byte b[]) throws IOException {  return read(b, 0, b.length);    }
    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }
    public long skip(long n) throws IOException {    return in.skip(n);    }
    public int available() throws IOException {     return in.available();    }
    public void close() throws IOException {     in.close();    }
    public synchronized void mark(int readlimit) {     in.mark(readlimit);    }
    public synchronized void reset() throws IOException {    in.reset();    }
    public boolean markSupported() {      return in.markSupported();    }
}
           

裝飾者模式:顧名思義,就是在一個已存在的對象上進行屬性或者功能的添加,比如一個已經存在的類,如果我們要進行功能和屬性上的擴充,一般是繼承它,然後新聲明一些成員變量并且重寫一些方法,方法的重寫是很煩的,而且耦合度也不小。

5、裝飾者會導緻出現很多小對象,如果過度使用,會讓程式變得複雜。

2.2.1、緩沖輸入流 BufferedInputStream

BufferedInputStream 繼承了 FilterInputStream,相當于是封裝者,而 FilterInputStream 裡面的 InputStream 相當于被封裝者,BufferedInputStream 為 InputStream 這個原始輸入流提供了更加獨特的操作接口。獨特在什麼地方呢?

獨特之處(作用):将原始輸入流中的位元組按多批次處理,沒次都取出一部分位元組存放于 BufferedInputStream 内部的緩沖區(實際也是位元組數組),然後我們讀取資料的目的地就放在了緩沖區,也許有人會困惑了,這樣做,不是沒事找事嗎,還這麼麻煩,直接讀取原始資料流不就行了嗎。我的了解是:如果原始資料流僅僅是對記憶體中的位元組數組的封裝,那 Buffered-InputStream 真的是搞麻煩了,但如果原始資料流是對磁盤中的資料的封裝(比如檔案流),那用 BufferedInputStream 會更加高效,因為 BufferedInputStream 是緩存區輸入流,一次性從磁盤中讀取很多位元組,減少了磁盤IO次數,而那些沒有緩沖的輸入流,讀取位元組都是一個一個地,會有着大量的磁盤IO。是以,BufferedInputStream 才有了存在的意義。

讀了源碼之後,有兩個值得學習的知識點:

1、MAX_BUFFER_SIZE 之是以是最大整數值 - 8,是因為有些虛拟機要在數組前面加一點資訊,是以預留一點空間。

2、判斷輸入流是否關閉的标準是,輸入流對象是否為null。

3、fill()方法的功能:從原始輸入流 InputStream 中讀取位元組資料,寫到緩沖區,盡量把緩沖區寫滿,然後對緩沖區裡的位元組進行讀寫。fill()的基本原理:為什麼是盡量寫滿呢?一般來講,緩沖區的資料讀過之後,就可以删除了,是以,按理說每次就從 InputStream 裡讀取固定大小資料,把buffer填滿,1 buffe 1 buffer 的讀取不就行了嗎,但是偏偏有個 markpos 标記功能,如果markpos = -1,說明沒有開啟标記功能,否則,說明開啟了标記功能,等會很可能還要讀一遍 markpos 之後的位元組資料,是以并不是将緩沖區的資料全部删除,而是需要把 markpos 之後的資料集中儲存在緩沖區前面,騰出緩沖區後面的空間, 然後再将 InputStream 裡的資料讀取一部分到緩沖區裡(未必填滿),然後更新 pos、count、markpos 等屬性,OK,我們又可以讀取 BufferedInputStream 裡的新資料啦。

使用者可以使用的方法:

1、public boolean markSupported():固定傳回true,因為支援 mark 和 reset 功能。

2、void mark(int readlimit):執行标記,不過需要設定marklimit  = readlimit。

3、public synchronized void reset():使得 markpos 标記起作用,讓 pos 指定 markpos 标記。

4、public synchronized int read(byte b[], int off, int len):讀取指定區間的位元組,用 off 和 len 去确定區間。

5、public synchronized int read():讀取下一個位元組,如果沒有可讀的了,就傳回 -1。

6、public synchronized int available():擷取剩餘可讀位元組數(是可以下一次不阻塞地讀取的),如果剩餘的特别多,就可能直接傳回Integer的最大值,否則,傳回原始 InputStream 剩餘的 + 緩沖區剩餘的。

7、public synchronized long skip(long n):讓讀取指針 pos 跳過 n 個位元組。

下面是 BufferedInputStream 類的源碼,部分方法的源碼沒有給出,但是會簡單介紹一下。

public
class BufferedInputStream extends FilterInputStream {
    // 預設緩沖區大小,機關位元組
    private static int DEFAULT_BUFFER_SIZE = 8192;
    // 緩沖區最大大小
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
    // 緩沖區,其實也是一個數組
    protected volatile byte buf[];

    // 這是一個原子更新器,用于更新緩沖區(位元組數組 buf)
    private static final
        AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
        AtomicReferenceFieldUpdater.newUpdater
        (BufferedInputStream.class,  byte[].class, "buf");
       
    // 緩沖區裡存儲的位元組總數量
    protected int count;
    // 下一個要讀取的位元組位置
    protected int pos;
    // mark标記,隻是在這裡類,換了個名字
    protected int markpos = -1;
    // 用于限制 markpos 和 pos 的最大距離,即頂多往之前讀marklimit個位置。
    protected int marklimit;
    // 擷取可用的輸入流 inputStream 對象
    private InputStream getInIfOpen() throws IOException {
        InputStream input = in;
        if (input == null)
            throw new IOException("Stream closed");
        return input;
    }

    // 獲得可用的緩沖區(位元組數組 buffer)
    private byte[] getBufIfOpen() throws IOException {
        byte[] buffer = buf;
        if (buffer == null)
            throw new IOException("Stream closed");
        return buffer;
    }

    // 構造方法
    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }

    // 構造方法,指定緩沖區大小
    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
    // 将原始 inputStream 流中的位元組資料讀取一部分到緩沖區中,在緩沖區裡處理。
    private void fill() throws IOException {
        // 省略具體實作代碼
    }

    // 從緩沖區讀取一個位元組,如果有必要,會填充緩沖區,如果原始輸入流 InputStream 裡的讀完了,就傳回 - 1.
    public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }
    // 跳過 n 個位元組
    public synchronized long skip(long n) throws IOException {
        //省略具體實作
    }
    // 傳回下一次能夠不阻塞地讀取的位元組數,原始InputStream裡的 + 緩沖區剩餘未讀的
    public synchronized int available() throws IOException {
        int n = count - pos;
        int avail = getInIfOpen().available();
        return n > (Integer.MAX_VALUE - avail)
                    ? Integer.MAX_VALUE
                    : n + avail;
    }
    // 關閉緩沖輸入流,即把自己的位元組數組buffer置為null,而且把被封裝者InputStream置為null
    public void close() throws IOException {
        byte[] buffer;
        while ( (buffer = buf) != null) {
            if (bufUpdater.compareAndSet(this, buffer, null)) {
                InputStream input = in;
                in = null;
                if (input != null)
                    input.close();
                return;
            }
        }
    }
}
           

2.2.2、 資料輸入流 DataInputStream

DataInputStream 是資料輸入流,繼承了 FilterInputStream 類,是以,正常的位元組輸入流的功能,它基本都有,比如幾個read 方法,但是沒有mark标記功能。但是又實作了 DataInput 接口,是以提供了比較有特色的功能。下面介紹一下特色功能:

1、readFully 方法:其實本質上是調用的 read 方法,read 方法有時會因為像網絡阻塞等外部原因而讀取不成功,但是readFully 内部有一個循環,直到成功讀取為止 或者 讀取完了才肯罷休。

2、skipBytes 方法:非要跳過指定數量的位元組,除非跳到末尾了。

3、readBoolean方法、readInt 方法等等,這些方法都是類似,比如readInt 方法,連續讀取4個位元組,然後拼成一個 int 并發傳回,跟上面講的 Bits 工具類裡的實作差不多。

4、readLine 方法:從原始輸入流中讀取一行位元組,碰到換行符或者回車符就算一行了,并将讀取到的一行轉為字元串并傳回

5、readUTF 方法:針對的是以utf-8格式編碼的輸入流,首先從該輸入流中讀取開頭2個位元組,這兩個位元組拼成short類型,這個short值是代表後面輸入流的長度(有多少位元組),然後根據short值建立一個位元組數組bytearr,将輸入流裡的全部位元組讀取出來存入位元組數組bytearr,再将bytearr轉換為字元數組chararr,最後處理字元數組,形成一個字元串,并傳回字元串。

浏覽 DataInputStream 的源代碼後,大概明白了這句話的含義:“是用來裝飾其它輸入流,它允許應用程式以與機器無關方式從底層輸入流中讀取基本 Java 資料類型”。我想它的意思是,機器底層無非也就是位元組流,但是我可以不關心位元組層面的,我可以以指定java基本資料類型去讀取流資料,然後讀取的資料就是java基本資料類型了。

但是也需要注意的是,如果你把一組short類型的資料轉為位元組流,然後你說你非要以int類型讀取該位元組流,雖然也能成功,但是讀取到的資料就有問題了。是以在使用DataInputStream時,需要注意一下,轉為位元組流前的類型或者編碼格式 和 讀取位元組流時所按照的讀取類型 或者 編碼格式 要一緻。

源碼如下,部分源碼沒有給出注釋,但是作了介紹。

public class DataInputStream extends FilterInputStream implements DataInput {
    // 構造方法,指定原始輸入流
    public DataInputStream(InputStream in) {
        super(in);
    }
    // 位元組數組 和 字元數組
    private byte bytearr[] = new byte[80];
    private char chararr[] = new char[80];
    // 從原始輸入流中讀取位元組存入數組 b
    public final int read(byte b[]) throws IOException {
        return in.read(b, 0, b.length);
    }
    // 從原始輸入流中讀取指定位置範圍的位元組存入數組 b
    public final int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }
    // 從原始輸入流讀取位元組,将b數組填滿,除非資料不夠了
    public final void readFully(byte b[]) throws IOException {
        readFully(b, 0, b.length);
    }
    // 確定能夠讀取到指定位置區間的那些位元組,除非資料已經讀完了
    public final void readFully(byte b[], int off, int len) throws IOException {
        if (len < 0)
            throw new IndexOutOfBoundsException();
        int n = 0;
        while (n < len) {
            int count = in.read(b, off + n, len - n);
            if (count < 0)
                throw new EOFException();
            n += count;
        }
    }
    // 非要跳過 n 個位元組,除非到末尾了
    public final int skipBytes(int n) throws IOException {
        int total = 0;
        int cur = 0;

        while ((total<n) && ((cur = (int) in.skip(n-total)) > 0)) {
            total += cur;
        }

        return total;
    }
    // 讀取一個位元組,然後轉換為boolean并傳回布爾值
    public final boolean readBoolean() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return (ch != 0);
    }
    // 讀取一個位元組,然後轉換為byte并傳回(有符号的位元組)
    public final byte readByte() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return (byte)(ch);
    }
    // 讀取一個位元組并傳回(本來就是無符号的)
    public final int readUnsignedByte() throws IOException {
        int ch = in.read();
        if (ch < 0)
            throw new EOFException();
        return ch;
    }
    // 一次性讀取2個位元組,将2個位元組轉換為short,并傳回short
    public final short readShort() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (short)((ch1 << 8) + (ch2 << 0));
    }
    public final int readUnsignedShort() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (ch1 << 8) + (ch2 << 0);
    }
    public final char readChar() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        if ((ch1 | ch2) < 0)
            throw new EOFException();
        return (char)((ch1 << 8) + (ch2 << 0));
    }
    public final int readInt() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        int ch3 = in.read();
        int ch4 = in.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0)
            throw new EOFException();
        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
    }
    private byte readBuffer[] = new byte[8];
    public final long readLong() throws IOException {
        readFully(readBuffer, 0, 8);
        return (((long)readBuffer[0] << 56) +
                ((long)(readBuffer[1] & 255) << 48) +
                ((long)(readBuffer[2] & 255) << 40) +
                ((long)(readBuffer[3] & 255) << 32) +
                ((long)(readBuffer[4] & 255) << 24) +
                ((readBuffer[5] & 255) << 16) +
                ((readBuffer[6] & 255) <<  8) +
                ((readBuffer[7] & 255) <<  0));
    }
    public final float readFloat() throws IOException {
        return Float.intBitsToFloat(readInt());
    }
    public final double readDouble() throws IOException {
        return Double.longBitsToDouble(readLong());
    }
    private char lineBuffer[];
    @Deprecated
    public final String readLine() throws IOException {
        char buf[] = lineBuffer;
        if (buf == null) {
            buf = lineBuffer = new char[128];
        }
        int room = buf.length;
        int offset = 0;
        int c;
loop:   while (true) {
            switch (c = in.read()) {
              case -1:
              case '\n':
                break loop;
              case '\r':
                int c2 = in.read();
                if ((c2 != '\n') && (c2 != -1)) {
                    if (!(in instanceof PushbackInputStream)) {
                        this.in = new PushbackInputStream(in);
                    }
                    ((PushbackInputStream)in).unread(c2);
                }
                break loop;
              default:
                if (--room < 0) {
                    buf = new char[offset + 128];
                    room = buf.length - offset - 1;
                    System.arraycopy(lineBuffer, 0, buf, 0, offset);
                    lineBuffer = buf;
                }
                buf[offset++] = (char) c;
                break;
            }
        }
        if ((c == -1) && (offset == 0)) {
            return null;
        }
        return String.copyValueOf(buf, 0, offset);
    } 
    public final String readUTF() throws IOException {
        return readUTF(this);
    }
    // 從UTF編碼格式的流中讀取出來内容,并以String的形式傳回
    public final static String readUTF(DataInput in) throws IOException {
        int utflen = in.readUnsignedShort();
        byte[] bytearr = null;
        char[] chararr = null;
        if (in instanceof DataInputStream) {
            DataInputStream dis = (DataInputStream)in;
            if (dis.bytearr.length < utflen){
                dis.bytearr = new byte[utflen*2];
                dis.chararr = new char[utflen*2];
            }
            chararr = dis.chararr;
            bytearr = dis.bytearr;
        } else {
            bytearr = new byte[utflen];
            chararr = new char[utflen];
        }
        int c, char2, char3;
        int count = 0;
        int chararr_count=0;
        in.readFully(bytearr, 0, utflen);
        while (count < utflen) {
            c = (int) bytearr[count] & 0xff;
            if (c > 127) break;
            count++;
            chararr[chararr_count++]=(char)c;
        }
        while (count < utflen) {
            c = (int) bytearr[count] & 0xff;
            switch (c >> 4) {
                case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                    /* 0xxxxxxx*/
                    count++;
                    chararr[chararr_count++]=(char)c;
                    break;
                case 12: case 13:
                    /* 110x xxxx   10xx xxxx*/
                    count += 2;
                    if (count > utflen)
                        throw new UTFDataFormatException(
                            "malformed input: partial character at end");
                    char2 = (int) bytearr[count-1];
                    if ((char2 & 0xC0) != 0x80)
                        throw new UTFDataFormatException(
                            "malformed input around byte " + count);
                    chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
                                                    (char2 & 0x3F));
                    break;
                case 14:
                    /* 1110 xxxx  10xx xxxx  10xx xxxx */
                    count += 3;
                    if (count > utflen)
                        throw new UTFDataFormatException(
                            "malformed input: partial character at end");
                    char2 = (int) bytearr[count-2];
                    char3 = (int) bytearr[count-1];
                    if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
                        throw new UTFDataFormatException(
                            "malformed input around byte " + (count-1));
                    chararr[chararr_count++]=(char)(((c     & 0x0F) << 12) |
                                                    ((char2 & 0x3F) << 6)  |
                                                    ((char3 & 0x3F) << 0));
                    break;
                default:
                    /* 10xx xxxx,  1111 xxxx */
                    throw new UTFDataFormatException(
                        "malformed input around byte " + count);
            }
        }
        return new String(chararr, 0, chararr_count);
    }
}
           

2.2.3、回推輸入流 PushbackInputStream

回推輸入流,為什麼叫這個名字呢?其它的輸入流都是被讀取,每讀取一個,pos 就往後移動,而這個輸入流是可以往裡寫入資料的,每寫一個位元組,pos 就往前移動。 PushbackInputStream 也是基于緩沖區的(位元組數組),但是它的緩沖區原理和 BufferedInputStream 不一樣,并且 PushbackInputStream 沒有标記功能,因為回推功能 和 标記功能 有點沖突,回推也是會改變 pos 值的,比如我回推2個位元組的資料, pos 就往前移動2個位置,但是此時 reset 了,mark起作用了,pos 的值就變成了mark值,豈不是把我回推的工作給覆寫了。

PushbackInputStream 繼承了 FilterInputStream 類,是以也是裝飾者模式裡的裝飾者,它自己有個緩沖區(位元組數組),另外也包含了原始輸入流,緩沖區的目的是存儲回推的位元組資料,不是用來存儲原始輸入流的資料,是以,和 BufferedInputStream 的原理不一樣。  PushbackInputStream 結構如下圖,它自己的緩沖區是綠色部分的,初始化時沒有内容,原始輸入流是橙色部分的,肯定有資料的,pos 初始化就指原始輸入流的第一個位元組處。

java 位元組輸入流/輸出流先序1、Bits 工具類 2、輸入流 InputStream3、輸出流 OutputStream

可能有人會問,回推具體是什麼操作呢? OK,先看一下回推方法 unread(int  b),確定原始輸入流式是打開的,然後 pos 指向了 0 位置(說明緩沖區滿了)的話,就抛出異常說明緩沖區已滿,如果沒有滿的話,就将 pos 指向前一個位置,并把要推入的資料 b 存入緩沖區的 pos 位置處。

是以,隻要我們隻執行回推操作,pos 就永遠不會指向緩沖區。

public void unread(int b) throws IOException {
        ensureOpen();
        if (pos == 0) {
            throw new IOException("Push back buffer is full");
        }
        buf[--pos] = (byte)b;
    }
           

它隻有2個構造方法:

1、public PushbackInputStream(InputStream in, int size):構造方法,指定原始輸入流,并指定緩沖區大小。

2、public PushbackInputStream(InputStream in):構造方法,指定原始輸入流,預設緩沖區大小為1。

常用方法(部分方法在源碼裡沒有給出,因為前文已經講得夠多了,不重複了):

1、public int read():讀取一個位元組。

2、public int read(byte[] b, int off, int len):讀取 len 個位元組存入 b 數組中,從 off 位置開始存。

3、public int available():擷取剩餘可讀位元組數(是可以下一次不阻塞地讀取的),如果剩餘的特别多,就可能直接傳回Integer的最大值,否則,傳回原始 InputStream 剩餘的 + 緩沖區剩餘的。

4、public long skip(long n):跳過 n 個位元組。

5、public void unread(int b):先檢視緩沖區是否存得下,存不下會抛出異常,存得下就會将 b 轉為位元組,然後存入。

6、public void unread(byte[] b, int off, int len):回推數組 b 的 off 位置開始的len個位元組 存入 緩沖區,如果存不下,抛出異常

7、public void unread(byte[] b):回推一個位元組。

8、public synchronized void close():關閉輸入流,釋放資源(緩沖區 + 原始輸入流)。

public class PushbackInputStream extends FilterInputStream {
    // 緩沖區
    protected byte[] buf;
    // 輸入流讀取指針,下一個将要讀取位元組的位置
    protected int pos;
    // 判斷輸入流是否開啟,否則抛出異常
    private void ensureOpen() throws IOException {
        if (in == null)
            throw new IOException("Stream closed");
    }
    // 構造方法
    public PushbackInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("size <= 0");
        }
        this.buf = new byte[size];
        this.pos = size;
    }
    // 構造方法
    public PushbackInputStream(InputStream in) {
        this(in, 1);
    }
    
    // 回推一個整數,隻不過整數要強制轉為 byte
    public void unread(int b) throws IOException {
        ensureOpen();
        if (pos == 0) {
            throw new IOException("Push back buffer is full");
        }
        buf[--pos] = (byte)b;
    }

    // 回推數組 b 的 off 位置開始的len個位元組 存入 緩沖區,如果存不下,抛出異常
    public void unread(byte[] b, int off, int len) throws IOException {
        ensureOpen();
        if (len > pos) {
            throw new IOException("Push back buffer is full");
        }
        pos -= len;
        System.arraycopy(b, off, buf, pos, len);
    }

    // 回推一個位元組
    public void unread(byte[] b) throws IOException {
        unread(b, 0, b.length);
    }
    // 關閉輸入流,釋放資源
    public synchronized void close() throws IOException {
        if (in == null)
            return;
        in.close();
        in = null;
        buf = null;
    }
}
           

2.3、檔案輸入流 FileInputStream

檔案輸入流專門用于從檔案裡讀取資料,它是沒有緩沖區的,是以沒有flush方法,磁盤IO的次數就會很多,效率不高。由于FileInputStream 内部多為本地方法,我也沒能力知曉具體原理,是以盡量介紹吧。

很多資料裡介紹說 FileInputStream 效率不高,沒有 FileChannel 的效率高,java.io 的效率比不上 java.nio,原因是頻繁地讀取磁盤會導緻速度變慢。

簡單說一下 FileDescriptor fd:檔案描述符對象,顧名思義就是描述一個檔案的資訊,比如,檔案号,有哪些線程在通路這個檔案,檔案的一些屬性等等。

至于其它幾個繼承 InputStream 的方法,這裡不重複說了。

另外, FileInputStream 包含了 FileChannel 成員變量,可是并沒有用到 FileChannel 呀,為什麼還要包含一個 FileChannel 變量呢?其實,FileChannel 是一個可選功能,FileInputStream 是單向的,隻能讀取磁盤,但是 FileChannel 是雙全工的,通過使用它,既能往磁盤寫,又能讀取磁盤,FileChannel 裡的方法是必須搭配 ByteBuffer 或者 ByteBuffer[] (可以被稱為緩沖區)使用的,是以 FileChannel 是可以批量讀取檔案的,是以磁盤IO的次數少,效率更高。FileChannel 有優勢,自然也有劣勢,是以不能一味地抛棄 FileInputStream,而隻用 FileChannel。

1、public FileChannel getChannel():獲得該檔案的檔案通道,然後通過這個檔案通道去寫入資料。

2、public FileInputStream(FileDescriptor fdObj):這個構造方法值得一說,為什麼要有這個方法呢?一般我們最常想到的就是直接用檔案路徑 或者 File 對象,很遺憾,FileInputStream 沒有可以擷取檔案路徑的方法,是以無法從一個FileInputStream 或者 FileOutputStream 對象裡擷取到檔案路徑,其次,在使用 FileInputStream、FileOutputStream、還有其他檔案流時,都會是先根據檔案路徑找到檔案,然後封裝成一個檔案描述符對象,反正都要封裝,還不如直接使用檔案描述符。

public class FileInputStream extends InputStream {
    // 檔案描述符
    private final FileDescriptor fd;
    // 帶檔案名的檔案路徑
    private final String path;
    // 檔案通道
    private FileChannel channel = null;
    private final Object closeLock = new Object();
    private volatile boolean closed = false;
    // 構造方法,指定檔案
    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }
    // 構造方法,指定File對象
    public FileInputStream(File file) throws FileNotFoundException {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkRead(name);
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
        fd = new FileDescriptor();
        fd.attach(this);
        path = name;
        open(name);
    }

    // 構造方法,指定檔案描述符
    public FileInputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkRead(fdObj);
        }
        fd = fdObj;
        path = null;
        fd.attach(this);
    }
    // 打開檔案 
    private void open(String name) throws FileNotFoundException {    open0(name);   }
    public int read() throws IOException {      return read0();    }
    private native int read0() throws IOException;
    private native int readBytes(byte b[], int off, int len) throws IOException;
    public int read(byte b[]) throws IOException {
        return readBytes(b, 0, b.length);
    }
    public int read(byte b[], int off, int len) throws IOException {
        return readBytes(b, off, len);
    }
    // 跳過 n 個位元組
    public long skip(long n) throws IOException {     return skip0(n);    }
    private native long skip0(long n) throws IOException;
    // 傳回檔案裡下次能不阻塞地讀取的位元組數
    public int available() throws IOException {    return available0();    }
    private native int available0() throws IOException;
    // 關閉檔案輸入流
    public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {             return;            }
            closed = true;
        }
        if (channel != null) {         channel.close();        }
        fd.closeAll(new Closeable() {
            public void close() throws IOException {           close0();           }
        });
    }
    // 擷取檔案描述符對象
    public final FileDescriptor getFD() throws IOException {
        if (fd != null) {
            return fd;
        }
        throw new IOException();
    }
    // 擷取檔案通道
    public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, true, false, this);
            }
            return channel;
        }
    }
}
           

3、輸出流 OutputStream

OutputStream 是一個抽象類,表示輸出流,我們的資料是往輸出流裡面寫入的。它定義基本的操作方法,3種 write 方法,特别說一下 flush() 方法,輸出流一般有一個位元組數組作為緩沖區,資料寫入輸出流後,先存儲在緩沖區,等到緩沖區滿了之後,或者沒有資料可寫入了,輸出流再調用 flush() 方法将緩沖區的資料重新整理入磁盤。這樣相當于按批存入磁盤,減少了磁盤 IO 次數。

public abstract class OutputStream implements Closeable, Flushable {
    // 将整數b轉為 byte,再将byte寫入此輸出流
    public abstract void write(int b) throws IOException;
    // 将位元組數組寫入此輸出流
    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }
    // 将位元組數組的off位置開始的len個位元組寫入此輸出流
    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]);
        }
    }
    // 将輸出流中緩沖區的資料立即重新整理到磁盤裡去
    public void flush() throws IOException {
    }
    // 關閉輸出流,釋放資源
    public void close() throws IOException {
    }
}
           

3.1、位元組數組輸出流 ByteArrayOutputStream

一般輸出流都是把資料寫入磁盤裡,但是這個 ByteArrayOutputStream 是将資料寫入記憶體中的一個緩沖區,說是緩沖區,其實就是一個位元組數組,把記憶體中的位元組存入到此緩沖區(位元組數組),這不是多此一舉嗎?其實你要說是多此一舉也行,但是比如你離散地擷取一些位元組,你把這些位元組存入 ByteArrayOutputStream,可以更加友善地管理,因為有很多現成的方法可用,但是你非要自己去管理得到的那些離散的位元組,也行,隻不過很麻煩,下次再碰到這種情況,你還得自己去寫一遍管理代碼。

ByteArrayOutputStream 更多地像一個存儲倉庫,用于緩存我們的一些位元組資料。

突然發現,ArrayList 和 ByteArrayOutputStream 很像,本質上都是由一個可變長度的資料實作的,隻不過 ByteArrayOutputStream 支援的是位元組,而 ArrayList 支援的 Object 類型,另外,在輸出流這個概念裡,ArrayList 沒有那些特色的功能(如果要有的話,也隻能自己額外編寫代碼實作了)。 

另外,ByteArrayOutputStream 的 close 關閉方法沒有用,因為緩沖區在記憶體中,是個位元組數組,當它不在被使用時,java虛拟機會自動清理它的。

介紹幾個常用方法:

1、public synchronized void write(int b):将整數轉為byte,再将byte寫入此輸出流

2、public synchronized void write(byte b[], int off, int len) :将位元組數組的off位置開始的len個位元組寫入此輸出流

3、public synchronized void writeTo(OutputStream out):将此輸出流的緩沖區資料寫入參數out輸出流

4、public synchronized void reset():将count置為0,意味着從緩沖區開頭開始寫入資料

5、public synchronized byte toByteArray()[] :擷取一份此輸出流的緩沖區(位元組數組),深拷貝

6、public synchronized int size():此輸出流的緩沖區的位元組數量

7、public synchronized String toString():将此輸出流的緩沖區裡的所有位元組按照預設的字元集編碼生成字元串

8、public synchronized String toString(String charsetName):将此輸出流的緩沖區裡的所有位元組按照指定的字元集編碼生成字元串

9、public void close():關閉輸出流,其實上面也沒有做

public class ByteArrayOutputStream extends OutputStream {
    // 緩沖區
    protected byte buf[];
    // 緩沖區内資料位元組數量
    protected int count;
    // 構造方法,預設緩沖區大小為32位元組
    public ByteArrayOutputStream() {     this(32);    }
    // 構造方法,指定緩沖區大小
    public ByteArrayOutputStream(int size) {
        if (size < 0) {
            throw new IllegalArgumentException("Negative initial size: "
                                               + size);
        }
        buf = new byte[size];
    }
    // 確定此輸出流緩沖區大小有minCapacity,如果不夠,就擴大緩沖區
    private void ensureCapacity(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - buf.length > 0)
            grow(minCapacity);
    }
    // 輸出流緩沖區最大大小
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = buf.length;
        int newCapacity = oldCapacity << 1;
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        buf = Arrays.copyOf(buf, newCapacity);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    // 将整數轉為byte,再将byte寫入此輸出流
    public synchronized void write(int b) {
        ensureCapacity(count + 1);
        buf[count] = (byte) b;
        count += 1;
    }
    // 将位元組數組的off位置開始的len個位元組寫入此輸出流
    public synchronized void write(byte b[], int off, int len) {
        if ((off < 0) || (off > b.length) || (len < 0) ||
            ((off + len) - b.length > 0)) {
            throw new IndexOutOfBoundsException();
        }
        ensureCapacity(count + len);
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }
    // 參數也是一個輸出流,意思是将此輸出流的緩沖區資料寫入指定輸出流裡
    public synchronized void writeTo(OutputStream out) throws IOException {
        out.write(buf, 0, count);
    }
    // 将count置為0,意味着從緩沖區開頭開始寫入資料
    public synchronized void reset() {      count = 0;    }
    // 擷取一份此輸出流的緩沖區(位元組數組),深拷貝
    public synchronized byte toByteArray()[] { return Arrays.copyOf(buf, count);   }
    // 此輸出流的緩沖區的位元組數量
    public synchronized int size() {    return count;    }
    // 将此輸出流的緩沖區裡的所有位元組按照預設的字元集編碼生成字元串
    public synchronized String toString() {  return new String(buf, 0, count);   }
    // 将此輸出流的緩沖區裡的所有位元組按照指定的字元集編碼生成字元串
    public synchronized String toString(String charsetName)
        throws UnsupportedEncodingException
    {
        return new String(buf, 0, count, charsetName);
    }
    // 關閉輸出流,其實上面也沒有做
    public void close() throws IOException {}
}
           

3.2、過濾輸出流 FilterOutputStream

這個也是裝修者模式的開始,FilterOutputStream 内部的原始輸出流 out 是被裝修者,這裡就不重複說了,看看前文的FilterInputStream類就可以了。

隻是close() 方法值得一學,關閉資源,是要先将緩沖區的資料刷入磁盤後,再關閉資源。

public class FilterOutputStream extends OutputStream {
    protected OutputStream out;
    public FilterOutputStream(OutputStream out) {     this.out = out;    }
    public void write(int b) throws IOException {     out.write(b);    }
    public void write(byte b[]) throws IOException {    write(b, 0, b.length);    }
    public void write(byte b[], int off, int len) throws IOException {
        if ((off | len | (b.length - (len + off)) | (off + len)) < 0)
            throw new IndexOutOfBoundsException();
        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
    }
    public void flush() throws IOException {     out.flush();    }
    @SuppressWarnings("try")
    public void close() throws IOException {
        try (OutputStream ostream = out) {
            flush();
        }
    }
}
           

3.2.1、緩沖輸出流 BufferedOutputStream

BufferedOutputStream 是裝修者,内部有個緩沖區(位元組數組),這個緩沖輸出流有什麼用呢?一般輸出流都是把資料寫入磁盤,如果每往輸出流裡寫一個位元組,就進行一次磁盤IO,那豈不是影響效率,是以有了 BufferedOutputStream,它是對原始輸出流進行了一層裝飾,每次寫入輸出流的位元組,并不是直接寫入到原始輸出流,而是先存到 BufferedOutputStream 的緩沖區裡,等緩沖區滿了,再将緩沖區的資料寫入原始輸出流(磁盤),這樣大大地減少了磁盤 IO 次數。 

其實上面把原始輸出流視為了寫入到磁盤,但是并不是所有的輸出流都這樣,比如 ByteArrayOutputStream 就可以作為原始輸出流,上面那樣寫是為了友善介紹。

值得注意的方法:

1、public synchronized void write(int b):将整數轉為byte,再将byte寫入此輸出流。

2、public synchronized void write(byte b[], int off, int len) :将位元組數組的off位置開始的len個位元組寫入此輸出流。

這兩個寫入方法,都是會檢查緩沖區是否還存得下,如果存不下,就将緩沖區的資料寫入到原始輸出流中,騰出空間了再存,如果原始輸出流都存不下了,那就會異常;

3、flush():重新整理的時候,是先将先把緩沖區裡的資料寫入原始輸出流裡,再把原始輸出流裡的資料刷入磁盤(未必是磁盤哦,因為萬一原始輸出流是 ByteArrayOutputStream這種, 根本沒有磁盤這一說)。

public class BufferedOutputStream extends FilterOutputStream {
    // 緩沖區,本質上是個位元組數組
    protected byte buf[];
    // 緩沖區中位元組資料的個數
    protected int count;
    // 構造方法,指定原始輸出流,預設設定緩沖區空間大小為8192
    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];
    }
    // 重新整理緩沖區,将緩沖區的資料全部寫入原始資料流。
    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }
    public synchronized void write(int b) throws IOException {
        if (count >= buf.length) {
            flushBuffer();
        }
        buf[count++] = (byte)b;
    }
    public synchronized void write(byte b[], int off, int len) throws IOException {
        if (len >= buf.length) {
            flushBuffer();
            out.write(b, off, len);
            return;
        }
        if (len > buf.length - count) {
            flushBuffer();
        }
        System.arraycopy(b, off, buf, count, len);
        count += len;
    }
    // 先把緩沖區裡的資料寫入原始輸出流裡,再把原始輸出流裡的資料刷入磁盤
    public synchronized void flush() throws IOException {
        flushBuffer();
        out.flush();
    }
}
           

3.2.2、DataOutputStream 資料輸出流

可以參數 DataInputStream 的了解,都差不多,DataOutputStream 也是對原始輸出流 out 的一個裝飾者,自身沒有緩沖區,它的絕大部分方法都是間接地調用原始輸出流的方法。DataOutputStream 的作用:允許将 java基本類型的資料 按照指定 java 基本類型的形式寫入輸出流。下面介紹幾個方法,看完你就明白了。

1、public final void writeByte(int v):将整數類型 v 轉換位 byte類型,這樣的話,byte 值就是 v 的低8位,然後再将 byte 值寫入輸出流。明顯會丢失一部分資料,但是為什麼還要這麼做呢?而且 DataOutputStream 裡大部分這種轉類型寫的方法的參數都是int,我覺得原因應該是和輸入流的read()方法有關,因為 int read() 方法讀取出來的一個位元組轉為了 int 傳回給我們,是以,你隻要和輸入流對上号,從輸入流 read 得到的 int,再把 int 寫入輸出流,資料就不會有問題了。

2、public final void writeShort(int v):這個是保留 int 值的低16位(2個位元組作為short類型值),再進行兩次寫入操作,一次寫入一個位元組。

3、public final void writeLong(long v):這個方法有點意思,将long分為8個位元組,按理說應該寫人8次,但是它卻沒有這麼做,它将8個位元組存入一個位元組數組裡,然後直接将這個位元組數組全部寫入,等同于寫8次位元組。我想它這麼做的原因是:如果原始輸出流的目的地是磁盤的話,那将減少磁盤IO次數,因為一次連續的寫肯定比8次斷續地寫要高效呀。

4、public final void writeFloat(float v):這個方法也值得我們學習,學習什麼呢?float的底層表示是采用 IEEE 754 浮點“單一格式”位布局,32位從高到底分别是 "一位做符号位,八位留作指數,23位作尾數",格式稍微複雜,每一位都有嚴格的意義。float 不允許執行位移操作,我們不能用 >>> 位移操作來擷取 float 的4個位元組,但是反正底層表示都是4個位元組,二進制的,對于同一個二進制,以不同資料類型的方式顯示出來的值會不一樣,但是底層的二進制是不會變的,是以用 Float.floatToIntBits(float a) 方法将 float 轉為 int(注意不是強制類型轉換,因為強制類型轉換會導緻精度丢失,而這個方法不會丢失),實際我覺得就是以 int 方式表示這個 4 個位元組,然後就可以使用位移操作了,就可以分别得到 float 的4個位元組了,最後将4個位元組存入輸出流。

5、public final void writeDouble(double v):原理同上。

6、public final void writeUTF(String str):将字元串轉換為字元數組,每個字元都用修改版utf-8 格式轉為位元組,然後将這些位元組寫入此資料輸出流。

7、static int writeUTF(String str, DataOutput out):同上,但是最後将位元組寫入指定資料輸出流。

public class DataOutputStream extends FilterOutputStream implements DataOutput {
    // 截至目前,已經往此資料輸出流寫入的位元組數
    protected int written;
    // 位元組數組,共某些方法使用的,不是緩沖區
    private byte[] bytearr = null;
    // 構造方法,指定原始輸出流
    public DataOutputStream(OutputStream out) {     super(out);    }
    // 将written值 + value
    private void incCount(int value) {
        int temp = written + value;
        if (temp < 0) {
            temp = Integer.MAX_VALUE;
        }
        written = temp;
    }
    // 把int轉為byte,再把byte寫入原始輸出流
    public synchronized void write(int b) throws IOException {
        out.write(b);
        incCount(1);
    }
    public synchronized void write(byte b[], int off, int len) throws IOException {
        out.write(b, off, len);
        incCount(len);
    }
    // 原始輸出流重新整理
    public void flush() throws IOException {     out.flush();    }
    // 将 boolean 值轉為 byte 值,再将 byte 寫入原始輸出流
    public final void writeBoolean(boolean v) throws IOException {
        out.write(v ? 1 : 0);
        incCount(1);
    }
    // 将 int 轉為 byte,再将 byte 寫入原始輸出流
    public final void writeByte(int v) throws IOException {
        out.write(v);
        incCount(1);
    }
    // 将 int 的低16位作為2個位元組(short)寫入原始輸出流
    public final void writeShort(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }
    // 将 int 的低16位作為2個位元組(char)寫入原始輸出流
    public final void writeChar(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }
    // 将 int 分為4個位元組,分别寫入原始輸出流
    public final void writeInt(int v) throws IOException {
        out.write((v >>> 24) & 0xFF);
        out.write((v >>> 16) & 0xFF);
        out.write((v >>>  8) & 0xFF);
        out.write((v >>>  0) & 0xFF);
        incCount(4);
    }
    private byte writeBuffer[] = new byte[8];
    // 将long分成8個位元組,然後分别寫入原始輸出流
    public final void writeLong(long v) throws IOException {
        writeBuffer[0] = (byte)(v >>> 56);
        writeBuffer[1] = (byte)(v >>> 48);
        writeBuffer[2] = (byte)(v >>> 40);
        writeBuffer[3] = (byte)(v >>> 32);
        writeBuffer[4] = (byte)(v >>> 24);
        writeBuffer[5] = (byte)(v >>> 16);
        writeBuffer[6] = (byte)(v >>>  8);
        writeBuffer[7] = (byte)(v >>>  0);
        out.write(writeBuffer, 0, 8);
        incCount(8);
    }
    // 将4位元組的float轉為4位元組的int(不丢失資料和精度),再将int寫入原始輸出流
    public final void writeFloat(float v) throws IOException {
        writeInt(Float.floatToIntBits(v));
    }
    // 将8位元組的double轉為8位元組的long(不丢失資料和精度),再将long寫入原始輸出流
    public final void writeDouble(double v) throws IOException {
        writeLong(Double.doubleToLongBits(v));
    }
    // 将字元串s拆分為一個一個的字元,每個字元占2位元組,但是強制将字元類型轉為byte類型,是以字元
    // 的低8位得以保留,高8位沒有了,再将這下byte存入原始輸出流
    public final void writeBytes(String s) throws IOException {
        int len = s.length();
        for (int i = 0 ; i < len ; i++) {
            out.write((byte)s.charAt(i));
        }
        incCount(len);
    }
    // 将字元串s拆分為一個一個的字元,每個字元占2位元組,将每個字元按照位元組寫入原始輸出流
    public final void writeChars(String s) throws IOException {
        int len = s.length();
        for (int i = 0 ; i < len ; i++) {
            int v = s.charAt(i);
            out.write((v >>> 8) & 0xFF);
            out.write((v >>> 0) & 0xFF);
        }
        incCount(len * 2);
    }
    // 将字元串 str 轉換為位元組序列,然後按照修改版的utf-8格式寫入此資料輸出流
    public final void writeUTF(String str) throws IOException {
        writeUTF(str, this);
    }
    // 将字元串 str 轉換為位元組序列,然後按照修改版的utf-8格式寫入指定的資料輸出流 out
    static int writeUTF(String str, DataOutput out) throws IOException {
        int strlen = str.length();
        int utflen = 0;
        int c, count = 0;
        /* use charAt instead of copying String to char array */
        for (int i = 0; i < strlen; i++) {
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                utflen++;
            } else if (c > 0x07FF) {
                utflen += 3;
            } else {
                utflen += 2;
            }
        }
        if (utflen > 65535)
            throw new UTFDataFormatException(
                "encoded string too long: " + utflen + " bytes");
        byte[] bytearr = null;
        if (out instanceof DataOutputStream) {
            DataOutputStream dos = (DataOutputStream)out;
            if(dos.bytearr == null || (dos.bytearr.length < (utflen+2)))
                dos.bytearr = new byte[(utflen*2) + 2];
            bytearr = dos.bytearr;
        } else {
            bytearr = new byte[utflen+2];
        }
        bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
        bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);
        int i=0;
        for (i=0; i<strlen; i++) {
           c = str.charAt(i);
           if (!((c >= 0x0001) && (c <= 0x007F))) break;
           bytearr[count++] = (byte) c;
        }
        for (;i < strlen; i++){
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                bytearr[count++] = (byte) c;
            } else if (c > 0x07FF) {
                bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
                bytearr[count++] = (byte) (0x80 | ((c >>  6) & 0x3F));
                bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            } else {
                bytearr[count++] = (byte) (0xC0 | ((c >>  6) & 0x1F));
                bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
            }
        }
        out.write(bytearr, 0, utflen+2);
        return utflen + 2;
    }
    // 已經往輸出流裡寫入的位元組數
    public final int size() {   return written;   }
}
           

3.3、檔案輸出流 FileOutputStream

檔案輸出流專門用于将資料寫入到檔案中,它是沒有緩沖區的,是以沒有flush方法,磁盤IO的次數就會很多,效率不高。由于FileOutputStream 内部多為本地方法,我也沒能力知曉具體原理,是以盡量介紹吧。

很多資料裡介紹說 FileOutputStream 效率不高,沒有 FileChannel 的效率高,java.io 的效率比不上 java.nio,原因是頻繁地磁盤寫入會導緻速度變慢,那就看看具體有哪些寫方法:

1、write(int b, boolean append):将 int 轉為 byte (保留 int 的低8位),再将 byte 寫入檔案,如果 append 參數為 true 的話,就将 byte 追加到檔案末尾(這樣不會丢失檔案原來的資料),如果 append 參數為 false 的話,就從檔案開頭開始寫入(會覆寫掉原來的内容),還有一個問題,為什麼不是将整個 int 寫入檔案呢,因為 FileOutputStream 是繼承 OutputStream 位元組流,如果不加特别說明,當然是以位元組為基準。

2、write(int b):功能同上,預設以覆寫形式寫入檔案(除非我們已經設定為追加方式了)。

3、void writeBytes(byte b[], int off, int len, boolean append):将位元組數組 b 的 off 位置開始的 len 個位元組寫入檔案,寫入的方式由 append 參數決定。底層也是一個位元組一個位元組的寫入檔案。

4、void write(byte b[]):将數組位元組 b 全部寫入檔案,寫入的方式取決于 append 成員變量。底層也是一個位元組一個位元組的寫入檔案。

5、void write(byte b[], int off, int len):将位元組數組 b 的 off 位置開始的 len 個位元組寫入檔案,寫入方式取決于 append 變量,底層也是一個位元組一個位元組的寫入檔案。

看了上面這幾個寫入資料的方法,也就明白了 FileOutputStream 是低效的。

但是 FileOutputStream 包含了 FileChannel 成員變量,可是并沒有用到 FileChannel 呀,為什麼還要包含一個 FileChannel 變量呢?其實,FileChannel 是一個可選功能,FileOutputStream 是單向的,隻能往磁盤裡寫,但是 FileChannel 是雙全工的,通過使用它,既能往磁盤寫,又能讀取磁盤,FileChannel 裡的方法是必須搭配 ByteBuffer 或者 ByteBuffer[] (可以被稱為緩沖區)使用的,是以 FileChannel 是批量寫入檔案的,是以磁盤IO的次數少,效率更高。FileChannel 有優勢,自然也有劣勢,是以不能一味地抛棄 FileOutputStream,而隻用 FileChannel。

6、public FileChannel getChannel():獲得該檔案的檔案通道,然後通過這個檔案通道去寫入資料。

至于幾個構造方法,看看下面的源碼注釋吧。

public class FileOutputStream extends OutputStream {
    // 檔案描述符對象
    private final FileDescriptor fd;
    // 是否是追加的方式
    private final boolean append;
    // 檔案通道
    private FileChannel channel;
    // 檔案路徑
    private final String path;
    private final Object closeLock = new Object();
    private volatile boolean closed = false;
    // 構造方法,給定檔案絕對路徑 + 檔案名
    public FileOutputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null, false);
    }
    // 構造方法,給定檔案絕對路徑 + 檔案名,是否以追加的方式
   public FileOutputStream(String name, boolean append) throws FileNotFoundException{
        this(name != null ? new File(name) : null, append);
    }
    // 構造方法,直接給定File對象
    public FileOutputStream(File file) throws FileNotFoundException {
        this(file, false);
    }
    // 構造方法,直接給定File對象,是否以追加的方式
    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();
        }
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
        this.fd = new FileDescriptor();
        fd.attach(this);
        this.append = append;
        this.path = name;

        open(name, append);
    }
    // 構造方法,直接指定檔案描述符對象
    public FileOutputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkWrite(fdObj);
        }
        this.fd = fdObj;
        this.append = false;
        this.path = null;

        fd.attach(this);
    }
    // 打開檔案,指定寫入方式
    private void open(String name, boolean append)
        throws FileNotFoundException {
        open0(name, append);
    }
    private native void write(int b, boolean append) throws IOException;
    public void write(int b) throws IOException {     write(b, append);    }
    private native void writeBytes(byte b[], int off, int len, boolean append)
        throws IOException;
    public void write(byte b[]) throws IOException {
        writeBytes(b, 0, b.length, append);
    }
    public void write(byte b[], int off, int len) throws IOException {
        writeBytes(b, off, len, append);
    }
    // 關閉檔案輸出流
    public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
            channel.close();
        }
        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();
           }
        });
    }
    // 擷取檔案描述符對象
     public final FileDescriptor getFD()  throws IOException {
        if (fd != null) {
            return fd;
        }
        throw new IOException();
     }
    // 擷取目前檔案的檔案通道
    public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, false, true, append, this);
            }
            return channel;
        }
    }
}
           

如有不正确的地方,歡迎指出,互相學習。