天天看點

Java IO之InputStream

Java io部分的知識是比較重要的一部分内容,io是了解nio的基礎,nio又是了解netty的基礎。

相信看到java io體系的結構圖的時候都會感歎他的龐大:

Java IO之InputStream

    在網上查閱相關資料的時候,也沒有一個很詳細的了解,大部分都是陳列一下api的用法,是以在這裡将自己對io的了解記錄下來。

   InputStream也就是io中的輸入流,用來處理位元組對象,也叫位元組流,他将資料以位元組的形式讀取到記憶體中。

   InputStream是一個抽象類,最主要的一個方法就是read()方法,無參就是一個位元組一個位元組的進行讀取資料,使用byte[]作為參數時,則是将讀取的位元組儲存到節數組中。

Java IO之InputStream

我們平時接觸到的都是他的子類,這裡主要通過以下幾個類來學習InputStream。

Java IO之InputStream
  • FileInputStream

FileInputStream是用于接收檔案資料的輸入流,他可以讀取檔案内容到記憶體中。

看一下它的兩個主要的構造方法:

  • 入參為檔案完整的路徑
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.incrementAndGetUseCount();
    this.path = name;
    open(name);
}
           

再看下read():

public int read() throws IOException {
    Object traceContext = IoTrace.fileReadBegin(path);
    int b = 0;
    try {
        b = read0();
    } finally {
        IoTrace.fileReadEnd(traceContext, b == -1 ? 0 : 1);
    }
    return b;
}
           
private native int read0() throws IOException;
           

這是無參的read(),一次讀取一個位元組,方法傳回一個int類型的資料,代表位元組對應的ASCLL碼。

read(byte[]):

public int read(byte b[]) throws IOException {
    Object traceContext = IoTrace.fileReadBegin(path);
    int bytesRead = 0;
    try {
        bytesRead = readBytes(b, 0, b.length);
    } finally {
        IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead);
    }
    return bytesRead;
}
           
private native int readBytes(byte b[], int off, int len) throws IOException;
           

和read()不同的是這個方法支援傳入一個位元組數組用來儲存讀取的位元組,也就是說如果你聲明了一個new byte[1024],那麼每次讀取的位元組數就将是1024個,他們全都儲存在這個位元組數組中,同時該方法也會傳回一個int,表示實際上讀取到的位元組數。

了解了上面這些,我們寫一個執行個體來看下效果,随便寫一個txt檔案,用FileInputStream看看是否可以讀取全部内容:

Txt内容:

就放五個字母 abcde來測試下

測試類:

package io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * 測試inputStream中的read的位元組
 */

/**
 * @author 18092106
 * @create 2018-08-31 15:18
 **/
public class TestInputStreamReadMethod {

    public void testReadOneByteOnce() {
        try(FileInputStream fis = new FileInputStream(new File("E:\\code\\leetcode\\read.txt"))){
            int hasRead = 0;
            int index = 0;
            while ((hasRead = fis.read()) != -1) {
                index++;
                System.out.println("read()方法第" + index + "次讀取的位元組是" + hasRead);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void testReadBytesOnce(){
        try (FileInputStream fis = new FileInputStream(new File("E:\\code\\leetcode\\read.txt"))) {
            byte[] bytes = new byte[2];
            int hasRead = 0;
            int index = 0;
            while((hasRead = fis.read(bytes)) != -1){
                index++;
                System.out.print("read(byte [])第" + index + "次讀取讀取了" + hasRead + "個位元組,byte[]中存放的位元組數量是" + bytes.length + ",分别是:");
                for (byte b:bytes) {
                    System.out.print(b + " ");
                }
                System.out.println();
            }



        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        new TestInputStreamReadMethod().testReadOneByteOnce();
        new TestInputStreamReadMethod().testReadBytesOnce();
    }
}
           

運作結果:

Java IO之InputStream

我們分别測試了兩種方式,可以看到read()一共執行了5次才讀取完檔案,read(byte[])中我們用了一個byte[2]來存放資料,一共讀了三次,最後一次實際上隻讀了一個位元組,但是byte裡面還是有兩個位元組,這又是為什麼呢?

   我們用一個圖來簡單解釋下這個過程:

Java IO之InputStream

第一次讀取a和b,a放入byte[0],b放入byte[1];

第二次讀取c和d,c放入byte[0],d放入byte[1];

第三次讀取e,e放入byte[0], byte[1]沒有修改。

  • ByteArrayInputStream

ByteArrayInputStream從命名就可以看出它是用來接收位元組數組類型的資料,他的用法和FileInputStream基本類似。

  • FilterInputStream(裝飾器模式)

   在java io流的設計中使用到了兩種設計模式,這裡的FilterInputStream就是用到其中的裝飾器模式,我們看下他的源碼:

protected FilterInputStream(InputStream in) {
    this.in = in;
}
           

這是它的構造函數,可以接收一個輸入流作為參數

再看下read()方法:

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);
}
           

可以看到,它并沒有實作任何自己的方法,所有的方法都是調用的構造函數中傳入的輸入流的方法,真正的實作都在它的子類中,子類繼承FilterInputStream,然後重寫對應的方法,實作相應的功能,這就是裝飾器起到的作用,我們也把這種需要其他節點流作為參數的流叫做處理流。

*裝飾器模式:裝飾模式是在不必改變原類檔案和使用繼承的情況下,動态的擴充一個對象的功能。它是通過建立一個包裝對象,也就是裝飾來包裹真實的對象。

  • BufferedInputStream

         BufferedInputStream就是繼承了FilterInputStream的一個處理流,它需要一個節點流作為構造函數的參數才能發揮作用,目的是為了優化原有的節點流,他也被叫做位元組緩沖流,我們一起看下BufferedInputStream的源碼來學習一下:

構造方法:

public BufferedInputStream(InputStream in) {
    this(in, defaultBufferSize);
}
           
public BufferedInputStream(InputStream in, int size) {
    super(in);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}
           

可以看到它需要另一個輸入流作為參數傳入,也就是被裝飾的對象,還有一個構造方法提供了一個int類型的參數,我們先放着,後面會介紹。

read(byte[]):

public synchronized int read(byte b[], int off, int len)
    throws IOException
{
    getBufIfOpen(); // Check for closed stream
    if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return 0;
    }

    int n = 0;
    for (;;) {
        int nread = read1(b, off + n, len - n);
        if (nread <= 0)
            return (n == 0) ? nread : n;
        n += nread;
        if (n >= len)
            return n;
        // if not closed but no bytes available, return
        InputStream input = in;
        if (input != null && input.available() <= 0)
            return n;
    }
}
           

可以看到主要讀取的方法是read1(),我們看下read1():

private int read1(byte[] b, int off, int len) throws IOException {
    int avail = count - pos;
    if (avail <= 0) {
        /* If the requested length is at least as large as the buffer, and
           if there is no mark/reset activity, do not bother to copy the
           bytes into the local buffer.  In this way buffered streams will
           cascade harmlessly. */
        if (len >= getBufIfOpen().length && markpos < 0) {
            return getInIfOpen().read(b, off, len);
        }
        fill();
        avail = count - pos;
        if (avail <= 0) return -1;
    }
    int cnt = (avail < len) ? avail : len;
    System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
    pos += cnt;
    return cnt;
}
           

可以看到 這邊有一個判斷:

我們看下getBuffIfOpen():

private byte[] getBufIfOpen() throws IOException {
    byte[] buffer = buf;
    if (buffer == null)
        throw new IOException("Stream closed");
    return buffer;
}
           

這裡面的buf對象就是上面我們第二個構造函數的size構造出來的一個位元組數組,如果沒有傳,那麼使用預設的一個值:

private static int defaultBufferSize = 8192;
           

也就是一個大小為8192的位元組資料 byte[8192],4K大小,我們繼續看剛剛的判斷:

這個len就是我們使用BufferedInputStream時自己構造的位元組資料的大小,如果我們構造的大小比預設的大了,就使用傳入的輸入流自身的read(byte[])的方法,如果沒有預設的大,那麼會調用一個fill()方法:

private void fill() throws IOException {
    byte[] buffer = getBufIfOpen();
    if (markpos < 0)
        pos = 0;            /* no mark: throw away the buffer */
    else if (pos >= buffer.length)  /* no room left in buffer */
        if (markpos > 0) {  /* can throw away early part of the buffer */
            int sz = pos - markpos;
            System.arraycopy(buffer, markpos, buffer, 0, sz);
            pos = sz;
            markpos = 0;
        } else if (buffer.length >= marklimit) {
            markpos = -1;   /* buffer got too big, invalidate mark */
            pos = 0;        /* drop buffer contents */
        } else {            /* grow buffer */
            int nsz = pos * 2;
            if (nsz > marklimit)
                nsz = marklimit;
            byte nbuf[] = new byte[nsz];
            System.arraycopy(buffer, 0, nbuf, 0, pos);
            if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                // Can't replace buf if there was an async close.
                // Note: This would need to be changed if fill()
                // is ever made accessible to multiple threads.
                // But for now, the only way CAS can fail is via close.
                // assert buf == null;
                throw new IOException("Stream closed");
            }
            buffer = nbuf;
        }
    count = pos;
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    if (n > 0)
        count = n + pos;
}
           

最核心的就是這個:

int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
           

還是調用輸入流自身的read(byte[])方法,但是這個位元組資料會使用BufferedInputStream中預設的那個,也就是byte[8192],通過這種位元組數組作為緩沖區,每次都從這裡取資料,不夠了就再去取8192個位元組數的資料回來。

說了這麼多理論,我們還是直接用代碼來測試一下,驗證一下我們的說法:

package io;

import java.io.*;

/**
 * @author 18092106
 * @create 2018-08-31 19:11
 **/
public class TestBufferedInputStream {

    public void useFileInputSream(){
        try (FileInputStream fis = new FileInputStream(new File("D:\\安裝包\\軟體安裝包.rar"))) {
            byte[] bytes = new byte[1024];
            int hasRead;
            long startTime = System.currentTimeMillis();
            while((hasRead = fis.read(bytes)) != -1){}
            long endTime = System.currentTimeMillis();
            System.out.println("檔案大小1.8G,FileInputStream用時:" + (endTime - startTime));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void useBufferedInputStream(){
        try (FileInputStream fis = new FileInputStream(new File("D:\\安裝包\\軟體安裝包.rar"));
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            byte[] bytes = new byte[1024];
            int hasRead;
             long startTime = System.currentTimeMillis();
            while((hasRead = bis.read(bytes)) != -1){}
            long endTime = System.currentTimeMillis();
            System.out.println("檔案大小1.8G,bufferedInputSream用時:" + (endTime - startTime));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }



    public static void main(String[] args) {
        new TestBufferedInputStream().useBufferedInputStream();
        new TestBufferedInputStream().useFileInputSream();
    }
}
           

運作結果:

Java IO之InputStream

我們選擇的節點流是FileInputStream,用FileInputStream和BufferedInputStream分别讀取一個同一個檔案,比較他們的用時。在聲明的位元組數組大小為1024時,可以明顯的看到BufferedInputStream的效率高很多,我們修改下聲明的位元組數組的大小為8192,也就是BufferedInputStream中預設大小,再來看看結果:

Java IO之InputStream

可以看到在這種情況下,反而FileInputStream的效率更高了,這個也和我們看源碼得到的結論一緻,總結下來就是:

BufferedInputStream内部構造了一個8192大小的位元組數組用作緩沖區,如果每次讀取的位元組數組小于這個預設的,那麼就使用這個預設的位元組數組來進行i/O通路,通過減少通路次數提高效率,當每次讀取的位元組數組大于這個預設的時候,還是調用傳入的輸入流本身的方法,這種情況由于内部的判斷等原因,反而輸入流的效率會更高。