Java io部分的知識是比較重要的一部分内容,io是了解nio的基礎,nio又是了解netty的基礎。
相信看到java io體系的結構圖的時候都會感歎他的龐大:
在網上查閱相關資料的時候,也沒有一個很詳細的了解,大部分都是陳列一下api的用法,是以在這裡将自己對io的了解記錄下來。
InputStream也就是io中的輸入流,用來處理位元組對象,也叫位元組流,他将資料以位元組的形式讀取到記憶體中。
InputStream是一個抽象類,最主要的一個方法就是read()方法,無參就是一個位元組一個位元組的進行讀取資料,使用byte[]作為參數時,則是将讀取的位元組儲存到節數組中。
我們平時接觸到的都是他的子類,這裡主要通過以下幾個類來學習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();
}
}
運作結果:
我們分别測試了兩種方式,可以看到read()一共執行了5次才讀取完檔案,read(byte[])中我們用了一個byte[2]來存放資料,一共讀了三次,最後一次實際上隻讀了一個位元組,但是byte裡面還是有兩個位元組,這又是為什麼呢?
我們用一個圖來簡單解釋下這個過程:
第一次讀取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();
}
}
運作結果:
我們選擇的節點流是FileInputStream,用FileInputStream和BufferedInputStream分别讀取一個同一個檔案,比較他們的用時。在聲明的位元組數組大小為1024時,可以明顯的看到BufferedInputStream的效率高很多,我們修改下聲明的位元組數組的大小為8192,也就是BufferedInputStream中預設大小,再來看看結果:
可以看到在這種情況下,反而FileInputStream的效率更高了,這個也和我們看源碼得到的結論一緻,總結下來就是:
BufferedInputStream内部構造了一個8192大小的位元組數組用作緩沖區,如果每次讀取的位元組數組小于這個預設的,那麼就使用這個預設的位元組數組來進行i/O通路,通過減少通路次數提高效率,當每次讀取的位元組數組大于這個預設的時候,還是調用傳入的輸入流本身的方法,這種情況由于内部的判斷等原因,反而輸入流的效率會更高。