Java InputStream 淺析

1 InputStream 子類結構

  • 下圖是 InputStream 及其子類的結構圖。可以看出 InputStream 是一個典型的裝飾者模式的示例
  • InputStream:抽象的組件類,具體的組件類繼承它,來實現不同的輸入流功能
  • FilterInputStream:抽象裝飾者類,具體裝飾者類繼承它,來對它包裝的組件類,進行擴展
  • Java IO 的擴展:
    • 添加具體組件類,實現特定的輸入流功能
    • 添加具體裝飾類,達到保證組件類的目的
      在這裏插入圖片描述

1.1 InputStream 源碼分析

  • inputStream抽象類功能
    • 定義讀取一個字節 或 多個字節的 方法
    • 定義了 mark、reset 的方法,由具體子類實現 重複讀取一段數據流的功能
    • 定義了關閉輸入流的方法
    • 定義了返回剩餘可讀取字節的方法
    • 注: 只有讀取一個字節的 read() 方法是抽象的,也就是說,具體類只要實現這一個方法即可,其它的可不實現
public abstract class InputStream implements Closeable {

    //返回下一個字節的 int 表示,如果輸入流已經結束了,則返回 -1。此方法會一直阻塞到有值返回,或拋異常
	public abstract int read() throws IOException;
    //從輸入流中讀取一些字節放入 字節數組 b 中,返回值即是讀取的真實字節數。此方法會一直阻塞到有值返回,或拋異常
    //如果 字節數組 b 的長度爲 0,則不會讀取任何字節,並且返回 0。如果輸入流已經結束則返回 -1
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    //和 read(byte b[]) 方法的區別僅僅是,讀出來的數據放入 字節數組的位置不一樣。此方法是從 字節數組的 off 索引處開始存放字節。返回值即是讀取的真實字節數
    //如果 字節數組 b 的長度爲 0,則不會讀取任何字節,並且返回 0。如果輸入流已經結束則返回 -1
    public int read(byte b[], int off, int len) throws IOException {...}

    //丟棄輸入流中的一些字節,丟棄的個數爲返回值。如果 n<=0,則不會操作流。
	public long skip(long n) throws IOException {...}
    //返回輸入流可以讀取的字節數(不準確),由子類實現
    public int available() throws IOException {
        return 0;
    }
    //關閉輸入流,並且釋放和流相關的系統資源
    public void close() throws IOException {}
    //標記當前輸入流的位置,這樣之後調用 reset() 方法時,將會重新將流設置到該標記的位置。實現重複讀取
    //如果 調用 mark() 方法後 又讀取了超過 readlimit 字節,mark 將會失效
    public synchronized void mark(int readlimit) {}

    //重新將流設置到 mark() 方法標記的位置,如果 mark() 已經失效則報錯
    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }
    //該輸入流是否可以被 mark
    public boolean markSupported() {
        return false;
    }
    //skip 方法最多跳過的字節數,如果 skip 的參數值大於此值,則實際使用的是此值
    private static final int MAX_SKIP_BUFFER_SIZE = 2048;
}

2 InputStream 具體實現類源碼分析

2.1 ByteArrayInputStream —— 字節輸入流

  • 核心: buf[] 數組,這個數組即是數據流的 源
    • buf[] 數組,通過構造函數傳入!
  • 字節流操作即是,操作數組的位置 pos。思想很簡單
  • 基本所有操作都被 synchronized 修飾
public class ByteArrayInputStream extends InputStream {
    protected byte buf[];//輸入流的源
    protected int pos;//輸入流當前位置
    protected int mark = 0;//標記的位置
    protected int count;//流的最大位置
    //返回當前位置的字節,並 pos 加 1
    public synchronized int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }
    //將流中的自己賦值到 字節數組 b 中,並更新流的位置
    public synchronized int read(byte b[], int off, int len) {...}
    //跳過 n 個字節
    public synchronized long skip(long n) {}
    //返回還有多少字節(準確)
    public synchronized int available() {}
    public void mark(int readAheadLimit) {
        mark = pos;//標記當前流位置,以便後面的 reset
    }
    public synchronized void reset() {
        pos = mark;//重置流到 mark 位置
    }
    //因爲 ByteArrayInputStream 流對其它資源無任何影響,所以不用任何操作
    public void close() throws IOException {}
    
    //用 buf[] 數組初始化流
    public ByteArrayInputStream(byte buf[]) {
        this.buf = buf;
        this.pos = 0;
        this.count = buf.length;
    }
    //用 buf[] 數組的 offset-length 階段初始化流
    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;
    }
}

2.2 FileInputStream —— 文件輸入流

  • 核心: file、fileDescriptor
    • 通過構造函數 傳入 fileName 或 File 類,內部創建一個 文件鏈接
  • 核心功能實現方法(本地方法):
    • read0():獲取下一個字節
    • readBytes(byte b[], int off, int len):從偏移量 off 開始,讀取 len 個字節,放入 b[] 數組中
    • skip0(long n):跳過 n 個字節
    • available0():還剩多少字節
    • close0():關閉文件流
  • 方法沒有被 synchronized 修飾,因爲最終調用的是本地方法,本地方法應該加了手段
public class FileInputStream extends InputStream {
    
    /* File Descriptor - handle to the open file */
    private final FileDescriptor fd;
    // 文件的路徑
    private final String path;
    //適配 java nio
    private FileChannel channel = null;

    private final Object closeLock = new Object();//保證只有一個線程調用 close 方法
    private volatile boolean closed = false;// double check 用途
    
    //通過打開一個與實際文件的連接,來創建一個 文件流。同時會創建一個 fileDescriptor 來標識這個連接。如果 file 不存在或是一個文件夾,拋異常。
    //如果存在 security manager 則會調用 security.checkRead(name) 方法驗證
    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }
    //上面的構造方法調用的就是這個,內部會調用 open() 方法
    public FileInputStream(File file) throws FileNotFoundException {...}
    //通過使用一個 fileDescriptor 來關聯一個已經存在的數據流。如果這個 fileDescriptor 無效,創建的時候不會報錯,當獲取 流 的時候就會報 IOException
    public FileInputStream(FileDescriptor fdObj) {...}
    //打開文件流
    private void open(String name) throws FileNotFoundException {
        open0(name);//native 方法
    }
    //從文件流中讀取一個字節,如果還沒有流準備好就會阻塞。如果流結束了則返回 -1
    public int read() throws IOException {
        return read0();
    }
    //讀取至多 len 字節數到 b 數組中,同 inputstream 方法
    private native int readBytes(byte b[], int off, int len) throws IOException;
    //讀取字節到 b 數組中,返回讀取的字節數
    public int read(byte b[]) throws IOException {
        return readBytes(b, 0, b.length);
    }
    //讀取到的字節放到 b 數組的 off 位置往後,最多讀取 len 字節
    public int read(byte b[], int off, int len) throws IOException {
        return readBytes(b, off, len);
    }
    //同 inputstream 方法
    public long skip(long n) throws IOException {
        return skip0(n);// native 方法
    }
    //同 inputstream 方法
    public int available() throws IOException {
        return available0();//native 方法
    }
    //關閉流相關資源
    public void close() throws IOException {
        //保證一個流只會 close 一次
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
           channel.close();//關閉 channel
        }
        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();//native 方法關閉流
           }
        });
    }
    //獲取 fileDescriptor
    public final FileDescriptor getFD() throws IOException {...}
    //獲取該 文件流 對應的 channel
    public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, true, false, this);
            }
            return channel;
        }
    }
    //最後的防護機制,防止用戶忘記關閉流
    protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            close();
        }
    }
}

2.3 PipedInputStream —— 管道輸入流

  • 管道輸入流,必須配合管道輸出流 (通過構造函數 傳入 一個 管道出入流)
  • 核心參數:
    • buffer[]:輸入流的存儲位置,只有輸出流,才能向此 buffer 中放入字節
    • in:可輸入的位置,-1表示輸入流沒啓動,輸出流 調用 receive() 方法時給 buffer[] 添加值,並 in++
    • out:可讀取的位置,從輸入流 執行 read() 方法時,讀取 buffer[out], 並out++
  • 核心方法:
    • receive():由輸出流調用,以此來將字節流傳入輸入流(buffer[] 數組中)
    • read():有輸入流調用,獲取輸入流的字節
public class PipedInputStream extends InputStream {
    protected byte buffer[];//輸入流的存儲位置,只有輸出流,才能向此 buffer 中放入字節!!
    protected int in = -1;//可輸入的位置,-1表示輸入流沒啓動,輸出流 調用 receive() 方法時給 buffer[] 添加值,並 in++
    protected int out = 0;//可讀取的位置,從輸入流 執行 read() 方法時,讀取 buffer[out], 並out++
    //一個輸入流的參數肯定是一個輸出流
    public PipedInputStream(PipedOutputStream src) throws IOException {
        this(src, DEFAULT_PIPE_SIZE);//默認的 buffer 爲 1024 大小
    }
    //設置 buffer 大小
    public PipedInputStream(PipedOutputStream src, int pipeSize)
            throws IOException {
         initPipe(pipeSize);
         connect(src);
    }
    //連接到 src 輸出流
    public void connect(PipedOutputStream src) throws IOException {
        //即,將 this(PipedInputStream 傳入 輸出流中,這樣輸出流,通過 write() 方法,寫入字節到,輸入流中(調用輸入流的receive方法))
        src.connect(this);
    }
    //這個是 PipedOutputStream 中的方法
    public synchronized void connect(PipedInputStream snk) throws IOException {
        if (snk == null) {
            throw new NullPointerException();
        } else if (sink != null || snk.connected) {
            throw new IOException("Already connected");
        }
        sink = snk;
        snk.in = -1;
        snk.out = 0;
        snk.connected = true;
    }
    //接收一個 b 自己到 輸入流中,in++。會阻塞(如生產者消費者一樣)
    protected synchronized void receive(int b) throws IOException {}
    //接收 b 數組的 off 到 off+len 字節,放入 buffer 中,in+len。會阻塞(如生產者消費者一樣)
    synchronized void receive(byte b[], int off, int len)  throws IOException {}
    //讀取一個字節,out++
    public synchronized int read()  throws IOException {}
    //讀取字節到 b 數組中,out+len。最多讀取 len 個
    public synchronized int read(byte b[], int off, int len)  throws IOException {}
    public void close()  throws IOException {
        closedByReader = true;
        synchronized (this) {
            in = -1;
        }
    }
}

2.4 ObjectInputStream —— Java 反序列化輸入流

  • 用於反序列化作用的流,其實相當於 裝飾者類,提供反序列化功能
// ObjectInputStream 用來反序列化,傳入的 inputStream 流!
// 只有 實現 Serializable 或 Externalizable 的類可以被 ObjectInputStream 反序列化
// 使用 readObject() 方法,讀取一個 object(如 String、arrays)
// 原始類型,可以通過 DataInput 中的方法來讀取
// 對象的默認反序列化機制將每個字段的內容還原爲其在寫入時的值和類型。反序列化過程將忽略聲明爲 transient 或 static 的字段。對其他對象的引用會根據需要從流中讀取這些對象。反序列化時總是分配新對象,這樣可以防止覆蓋現有對象。
//讀取對象類似於運行新對象的構造函數。爲對象分配內存並初始化爲零(空)
/*
    FileInputStream fis = new FileInputStream("t.tmp");
    ObjectInputStream ois = new ObjectInputStream(fis);

    int i = ois.readInt();
    String today = (String) ois.readObject();
    Date date = (Date) ois.readObject();
    //因爲 ObjectInputStream 只是 FileInputStream 包裝類,關閉它實際上關閉的就是具體的 FileInputStream 數據流
    ois.close();
*/
//如果在序列化或反序列化的時候執行一些特殊的邏輯,可以在實現類中重寫 writeObject、readObject、readObjectNoData 方法
//當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable接口
//當一個對象的實例變量引用其他對象,序列化該對象時也把引用對象進行序列化
public class ObjectInputStream extends InputStream {
    //存儲傳入的 輸入流(如 fileinputstream)
    private final BlockDataInputStream bin;
    //接收一個 輸入流
    public ObjectInputStream(InputStream in) throws IOException {}
    //反序列化一個對象
    public final Object readObject() throws IOException, ClassNotFoundException {}
    //和 readObject() 方法功能相同,只是當調用此方法返回一個對象後,如果後面有返回一個同樣的對象,就會報錯(即使用 readUnshared() 返回的對象必須在流中是唯一的)
    public Object readUnshared() throws IOException, ClassNotFoundException {}
    //從流中讀取持久字段並按名稱使其可用
    public ObjectInputStream.GetField readFields() {}
    //以下是反序列化基本類型數據 ---------------
    public int read() throws IOException {}
    public int read(byte[] buf, int off, int len) throws IOException {}
    public boolean readBoolean() throws IOException {}
    public byte readByte() throws IOException  {}
    public int readUnsignedByte()  throws IOException {}
    public char readChar()  throws IOException {}
    public short readShort()  throws IOException {}
    public int readUnsignedShort() throws IOException {}
    public int readInt()  throws IOException {}
    public long readLong()  throws IOException {}
    public float readFloat() throws IOException {}
    public double readDouble() throws IOException {}
    //直接在輸入流中讀取字節到 數組 b 中
    public void readFully(byte[] buf) throws IOException {}
    public void readFully(byte[] buf, int off, int len) throws IOException {}
    //讀取 UTF 對應的字節作爲字符
    public String readUTF() throws IOException {}
}

2.4.1 BlockDataInputStream

  • 是 ObjectInputStream 的內部類

  • 具有兩種模式的輸入流

    • 在“默認”模式下,以與dataoutputstream相同的格式輸入數據
    • 在“塊數據”模式下,輸入流以塊的形式被緩存。當處於默認模式時,不預先緩衝數據;當處於塊數據模式時,立即讀取當前數據塊的所有數據(並緩衝)。
  • 塊模式就相當於給輸入流加了一層緩衝區

private class BlockDataInputStream extends InputStream implements DataInput {
    /** block data mode */
    private boolean blkmode = false;//默認不是塊數據模式,需要設置 setBlockDataMode()
	//包裝輸入流,達到預期功能作用
    BlockDataInputStream(InputStream in) {
        this.in = new PeekInputStream(in);
        din = new DataInputStream(this);//裝飾者組件
    }
    //獲取字節流,但不消耗!read() 方法消耗數據流!
    int peek() throws IOException {
        if (blkmode) {
            if (pos == end) {
                refill();
            }
            return (end >= 0) ? (buf[pos] & 0xFF) : -1;
        } else {
            return in.peek();
        }
    }
    //當塊緩存區消耗完了後,就調用此方法,讀取輸入流中數據到緩存區中
    private void refill() throws IOException {...}
    
    public float readFloat() throws IOException {
        if (!blkmode) {
            pos = 0;//如果不是塊模式則去 peekInputStream 中讀取流,放入 buf 中
            in.readFully(buf, 0, 4);
        } else if (end - pos < 4) {
            return din.readFloat();//如果是塊模式,但是緩存區字節不夠
        }
        float v = Bits.getFloat(buf, pos);
        pos += 4;
        return v;
    }
}

2.4.2 PeekInputStream

  • 是 ObjectInputStream 的內部類

  • 提供了獲取字節流但不消耗字節流的功能

  • 提供了統計獲取字節數的功能

private static class PeekInputStream extends InputStream {
    //獲取字節流但不消耗字節流!下一次 read() 操作又會將 peekb 賦值爲 -1!
    int peek() throws IOException {
        if (peekb >= 0) {
            return peekb;
        }
        peekb = in.read();
        totalBytesRead += peekb >= 0 ? 1 : 0;
        return peekb;
    }
}

2.5 SocketInputStream —— 網絡輸入流

  • socket 網絡連接使用的 輸入流
  • 核心方法:
    • socketRead0(): 本地方法,得到網絡輸入流字節,讀到 -1 時,表示流結束
    • read():內部調用 socketRead0() 方法
class SocketInputStream extends FileInputStream {
    SocketInputStream(AbstractPlainSocketImpl impl) throws IOException {
        //通過 AbstractPlainSocketImpl 得到 fileD 來打開(標誌)一個文件流,並通過這個關閉輸入流,和得到其它一些參數(如,timeout)
        super(impl.getFileDescriptor());
        this.impl = impl;
        //通過 AbstractPlainSocketImpl 得到 一個 socket,用來關閉輸入流
        socket = impl.getSocket();
    }
    private int socketRead(FileDescriptor fd,byte b[], int off, int len,int timeout) {
        //讀取 socket 流到 b 字節數組中,超時時間爲 timeout,偏移爲 off,最大長度爲 len
        return socketRead0(fd, b, off, len, timeout);
    }
    //Reads into a byte array data from the socket.如果流結束了,則返回 -1
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    public void close() throws IOException {
        // Prevent recursion. See BugId 4484411
        if (closing)
            return;
        closing = true;
        if (socket != null) {
            if (!socket.isClosed())
                socket.close();//關閉 socket 
        } else
            impl.close();//如果 socket 爲 null 還需要關閉 impl
        closing = false;
    }
}

3 Inputstream 裝飾者類源碼分析

3.1 BufferedInputStream —— 緩衝輸入流

  • 通過構造函數,包裝一個 輸入流,對這個輸入流做緩衝,同時實現 mark 和 reset 的功能
  • 核心字段
    • buf[]:用來緩衝輸入流的字節數組
    • count:buf 字節數組能讀取到的最大下標
    • pos:目前讀取 buf[] 的索引
    • markpos :調用 reset 後,pos 會被設爲 markpos
    • marklimit:每次 mark 的時候,設置一個值,超過這值時,mark 就失效了(markpos 設爲 -1)
  • 核心方法:
    • fill():用來填充 buf[],當緩衝不夠時
    • read():從緩衝中讀取一個字節
    • read1(b,off ,len):從緩衝中的 off 偏移,讀 len 字節到 b 字節數組中
//bufferedInputStream 通過包裝 具體 inputstream ,使用一個 buffer 字節數組,實現了輸入流緩存功能,同時實現了 mark 和 reset 的功能
public class BufferedInputStream extends FilterInputStream {
    protected volatile byte buf[];
    protected int count;//buf 字節數組能讀取到的最大下標
    protected int pos;//目前讀取 buf[] 的索引
    protected int markpos = -1;//調用 reset 後,pos 會被設爲 markpos
    protected int marklimit;//每次 mark 的時候,設置一個值,超過這值時,mark 就失效了(markpos 設爲 -1)
    //包裝 inputstream,並設置 buffer 字節數組大小
    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
    public synchronized void mark(int readlimit) {
        marklimit = readlimit;
        markpos = pos;
    }
    public synchronized void reset() throws IOException {
        getBufIfOpen(); // Cause exception if closed
        if (markpos < 0)
            throw new IOException("Resetting to invalid mark");
        pos = markpos;
    }
    //從 buffer 數組中讀取下一個字節
    public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }
    //重新從輸入流中讀取一定字節存儲到 buffer 數組中,並更新 pos 和 count!
    private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();
        if (markpos < 0)
            pos = 0;//重置 pos   
        else {//複雜情況暫不考慮
        }
        count = pos;
        //從輸入流中讀取數據到 buffer 中,返回讀取到的數據
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
        if (n > 0)
            count = n + pos;//count 索引就是 buffer 數組有效字節的最大位置
    }
}

3.2 DataInputStream —— 基本類型反序列化輸入流

  • 通過構造函數,傳入輸入流,提供對輸入流進行基本類型反序列化功能

  • 完全可以配合 BufferInputStream

  • 核心方法:

    • readFully(b, off, len):從輸入流中讀取字節,一直循環讀取到 len 字節才返回,如果一直到沒有字節了還沒讀滿,則報錯
    • readBoolean():讀取下一個字節,如果不是 0,則返回 true,否則返回 false
    • readLine():此方法不能正確的將字節轉爲字符,被 BufferedReader 取代
    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
    reader.readLine();
    
    • readUTF():獲取下一個 UTF-8 的字符
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));
}
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 byte readByte() throws IOException {
    int ch = in.read();
    if (ch < 0)
        throw new EOFException();
    return (byte)(ch);
}

3.3 GZIPInputStream —— 讀取GZIP 格式輸入流

  • 內部原理留待以後分析
  • 估計是 readTrailer() 方法起主要作用
public class GZIPInputStream extends InflaterInputStream {
    /**
     * CRC-32 for uncompressed data.
     */
    protected CRC32 crc = new CRC32();

    /**
     * Indicates end of input stream.
     */
    protected boolean eos;

    private boolean closed = false;
    public GZIPInputStream(InputStream in, int size) throws IOException {
        super(in, new Inflater(true), size);
        usesDefaultInflater = true;
        readHeader(in);
    }
    public int read(byte[] buf, int off, int len) throws IOException {
        ensureOpen();
        if (eos) {
            return -1;
        }
        int n = super.read(buf, off, len);
        if (n == -1) {
            //估計是 readTrailer() 方法起主要作用
            if (readTrailer())
                eos = true;
            else
                return this.read(buf, off, len);
        } else {
            crc.update(buf, off, n);
        }
        return n;
    }
}

3.4 ZIPInputStream —— 讀取ZIP 格式輸入流

  • 內部原理留待以後分析
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章