文章目錄
- Java基礎知識-03-C-2020-07-22
-
- 寫在前頭
- I/O
-
- I/O開始正片之前的雜序
- I/O-位元組流-ByteArray
- I/O-位元組流-Piped
- I/O-位元組流-Filter
-
- I/O-位元組流-Filter-Buffered
- I/O-位元組流-Filter-Data
- I/O-位元組流-Object
- I/O-位元組流-File
- I/O-字元流-CharArray
- I/O-字元流-Piped
- I/O-字元流-Filter
- I/O-字元流-Buffered
- I/O-字元流-InputStreamReader/OutputStreamWriter
- I/O-字元流-File
- I/O-圖上有兩個沒講的在這裡
Java基礎知識-03-C-2020-07-22
日志編号 | 說明 |
---|---|
C-2020-07-22 | 第一次建立 |
寫在前頭
針對下面的内容,我并沒有說推薦按順序看,或者挑需要的類看。
唯一的建議,假設想挑需要的類去看的話,至少在看你需要的類之前,先耐着性子的看一下I/O-字元流-ByteArray這個小章節。作為正片的第一個小節,我有詳細的介紹它裡面的每個方法,每一個構造方法,還有各種各樣的概念。這些方法和概念很多都是繼承自InputStream,OutputStream,是以越往後,随着篇幅的增長,這些重複的基本概念和方法我要麼沒寫,要麼一筆帶過,有時候不利于了解。是以推薦看一下ByteArray章節之後,再挑選需要的類去看。
I/O
先上圖

I/O開始正片之前的雜序
首先,上圖中的内容,非常的不全。我隻是列舉了基礎的應知應會的子類。
I/O流,一個從基礎到高階,自始至終都會存在的一個必須具備的知識。I/O流中有很多,各式各樣的知識,各式各樣的坑。這裡光是記錄基礎知識。
從圖上可以看到,I/O基本可以分為兩大類,字元流和位元組流。
-
位元組流
按照byte[]位元組數組去讀取和寫入的流,這種流啥都能幹,一般以Stream結尾。
-
字元流
按照字元去讀取和寫入的流,更适合于處理文本資訊,一般以Reader/Writer結尾。
除去位元組流和字元流,還可以劃分成節點流和處理流(包裝流)。節點流是實際幹活的流,如FilterInputStream,處理流是在節點流上包裝的流,如InputStreamReader。這種劃分,在實際使用裡有更好的體會,是以對編碼經驗也比上面那種分類提出了更高的要求。為什麼會出現節點流和處理流?因為節點流的API雖然啥都能幹,但是幹啥都覺得不好用。于是處理流的存在,就是在開發人員和節點流之間建構一道友善的橋梁。處理流對外提供的功能非常友善。
除此之外,還有一種常見的分類,就是輸入流和輸出流。輸入流是InputStream和Reader作為基類,輸出流是OutputStream和Writer作為基類。
本文采用了位元組流和字元流的分類方式。
仔細觀察上圖會發現,不論是在字元流或者位元組流的分類下,裡面的内容基本上都是成對出現的,也就是一個input一般都會有一個output,意義是輸入/輸出。對于計算機而言,什麼是輸入,什麼是輸出?通俗來講,往程式裡面讀是輸入流,從程式裡往其他地方寫入,是輸出流。
上面的描述中寫道了基本上,結合圖例會發現在FilterOutputStream中,比FilterInputStream裡多了一個PrintStream類。其實這個類在日常工作中經常用到,就是System.out 。在Java源碼中,是如下定義的。
如上圖所示,常用的System.out和System.err都是PrintStream對象。除過這兩個,還有個标準輸入流System.in,在Java源碼中,定義的是InputStream類型,但是InputStream是個抽象類,是以在最終執行個體化的過程裡肯定是用InputStream的一個子類。
從代碼上的注釋和明确看到,這個對象是響應鍵盤或者其他輸入裝置的輸入資訊。是以是更底層的東西。之是以猜測是更底層的東西去實作,如果要深究,我覺得是需要從System類的initializeSystemClass方法作為入口往裡看。
下面I/O部分寫作說明,下面的次級目錄,按照xmind中上而下,從位元組流到字元流,把成對的輸入輸出流放在一起進行記錄。比如I/O-位元組流-ByteArray表示的是,這個子目錄裡面會将I/O位元組流中的ByteArrayInputStream和ByteArrayOutputStream。
I/O-位元組流-ByteArray
ByteArray,字面意思是位元組數組,說白了就是byte[]。
ByteArrayInputStream屬于java.io包,是InputStream類的子類。
在ByteArrayInputStream類中,除過自身的構造方法之外并沒有自己新定義什麼方法,它的方法均是繼承自InputStream中的方法(JDK8)
-
構造方法
ByteArrayInputStream中提供了兩個構造方法。
/**
* Creates a <code>ByteArrayInputStream</code>
* so that it uses <code>buf</code> as its
* buffer array.
* The buffer array is not copied.
* The initial value of <code>pos</code>
* is <code>0</code> and the initial value
* of <code>count</code> is the length of
* <code>buf</code>.
*
* @param buf the input buffer.
*/
public ByteArrayInputStream(byte buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
上述構造方法裡傳入一個byte[]作為入參,官方注解中講buf[]是輸入緩沖區。通俗一些的解釋,ByteArrayInputStream是一個輸入流,輸入流就是說明要從某個地方A讀取資訊,那此時,就是說把A的整體或者通過循環之後,把A的部分内容轉成buf[],然後操作流對象從buf[]中讀取。這個就是輸入緩沖區的了解。
pos是表示從緩沖區的什麼位置開始讀取,count讀取到什麼位置為止。從代碼上可以看出,每次讀取了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;
}
和第一個構造方法不同的是,第二個構造方法中多了兩個參數。offset表示讀取的偏轉量,原先是從第一個位置開始,現在是從指定位置開始。length是每次讀取的長度。從代碼中可以看到count此時等于(偏轉量+長度)或者(buf長度)兩者中的最小值。并且把mark的值設定成偏移量。
- 其他方法
-
read():int
read()表示每次從上一次讀取的位置開始,讀取下一個資料位元組,并傳回一個int值(0~255),如果已經把整個buf[]都讀取完了,會傳回(-1)這個特征值表示讀取結束。
-
read(byte[],int,int):int
和上面的read()方法類似,都是資料的功效,但是在實際使用中存在不同的地方。
方法聲明部分和參數注釋如下:
Java基礎知識-03-C-2020-07-22Java基礎知識-03-C-2020-07-22 第一個參數byte[] b 的作用是将ByteArrayInputStream中的資料讀取到入參b中。
第二個參數 int off,是起始偏移量。
第三個參數 int len,每次要讀取的長度。如果剩餘長度小于len,會把剩餘長度賦予len。
這個方法和read方法類似,都是傳回一個int值,不同的是,read()傳回的是讀取的内容,這個方法傳回的是讀取的長度,而讀取的内容則是在入參 b中。
這個方法的傳回值有如下意義:
如果傳回-1,說明已經讀取到最後的内容,這一次沒有讀到新的内容。
如果傳回0,則說明沒有可以讀取的内容了。
如果傳回其他值,則表示讀取的位元組串長度。
-
skip(long):long
傳入一個long值 n,從輸入流中目前執行的位置跳過n個位元組。如果輸入流中剩餘位元組數 < n 的值,則隻會跳過剩餘的位元組數。
這個方法的傳回值也是個long值,會把實際跳過的位元組個數進行傳回。
-
available():int
這個方法會把從目前執行的位置,到流對象的結尾,中間所有不阻塞地可讀取(或者可跳過)的位元組數量進行傳回。判斷這個流有沒有内容的時候可以用這個方法。
-
markSupported():boolean
這個方法是和下面的mark和reset方法相關聯的。
首先,先看一下ByteArrayInputStream,從名字上看有兩個核心意思,一個是ByteArray一個是InputStream。一個是位元組數組,一個是輸入流。這個方法和下面兩個方法,是應對ByteArrayInputStream中ByteArray特性而言的。
數組資料,說個簡單的,一個int[],在數組裡,有自己的下标,我們可以通過下标周遊數組裡的對象,同時也可以說從第幾個開始周遊。那這裡面就牽扯到兩個概念,一個是mark(标記),一個是重置(reset)。标記的意思就是,從第幾個開始。重置的意思就是把下标重新放在最開始。
因為ByteArrayInputStream是InputStream的子類,而InputStream有好多好多子類,那麼就需要一個方法,來表示各個子類(如ByteArrayInputStream)是否滿足标記和重置操作。
對于ByteArrayInputStream來講,它是支援标記和重置的,是以這個方法永遠傳回true。
-
mark(int):void
标記,也就是指定下标的位置。
-
reset():void
重置,把pos目前位置設定成之前mark時候的位置。如果之前沒有mark過,那就會把pos設定成0。
-
close():void
close方法繼承自InputStream,InputStream類是實作了Closeable接口。
這個方法有些特殊,具體代碼如下:
-
/**
* Closing a <tt>ByteArrayInputStream</tt> has no effect. The methods in
* this class can be called after the stream has been closed without
* generating an <tt>IOException</tt>.
*/
public void close() throws IOException {
}
這個方法是空的,關閉ByteArrayInputStream沒有任何效果。可以在流關閉後調用此類中的方法,而不生成IOException。 這個部分我沒有深究什麼,使用中也是在read()之類的方法傳回-1的時候,調一下。
ByteArrayOutputStream是java.io包下的類,是OutputStream的子類。
與ByteArrayInputStream的介紹順序一樣,還是從構造函數和其他方法這兩個次元進行講解。
-
構造方法
ByteArrayOutputStream類中有兩個構造方法,代碼如下。
/**
* Creates a new byte array output stream. The buffer capacity is
* initially 32 bytes, though its size increases if necessary.
*/
public ByteArrayOutputStream() {
this(32);
}
第一個構造方法不用傳遞參數,在初始化ByteArrayOutputStream的時候建立一個對象,并且把輸出緩沖區的長度設定成32
/**
* Creates a new byte array output stream, with a buffer capacity of
* the specified size, in bytes.
*
* @param size the initial size.
* @exception IllegalArgumentException if size is negative.
*/
public ByteArrayOutputStream(int size) {
if (size < 0) {
throw new IllegalArgumentException("Negative initial size: "
+ size);
}
buf = new byte[size];
}
第二個構造方法是建立一個指定緩沖區大小的ByteArrayOutputStream對象。
- 其他方法
-
write(int):void
将指定的位元組(就是傳入的int值)寫入到目前的位元組流中。
-
write(byte[]): void
把byte[]中的資料寫到目前輸出流中。
-
write(byte[],int,int):void
這個方法裡有三個參數。
第一個參數byte[],表示的意思是要把這個數組中的内容寫入到目前的位元組流對象中。
第二個int off,針對byte[] 要從第幾個元素開始把它的内容寫入到位元組流中,即起始的偏移量。
第三個int len,表示從off開始,将多長(len)的數組元素寫入到位元組流中。
-
writeTo(OutputStream):void
将目前的輸出流中的全部内容寫入到傳入的輸出流對象中。
-
reset():void
将ByteArrayOutputStream對象中的count值設成0,意味着目前輸出流裡已經寫的所有内容将被丢棄,目前的輸出流可以被重用,并且可以重用之前聲明的輸出緩沖區。
-
toByteArray():byte[]
建立一個新的byte[],将目前輸出流的内容拷貝到新建立的byte[]中,并傳回。
-
size():int
傳回目前輸出流的大小,這裡說的大小,可以直覺的了解為目前輸出流内已經有了多少個要用于資料的位元組。
-
toString(String):String
按照傳入的字元集(這個方法的入參是字元集的名稱)将輸出流中的内容建立成一個String對象,并傳回。
-
toString(int):String
已經過時的方法,不進行贅述
-
close():void
這個方法與ByteArrayInputStream中的一樣,沒有執行的代碼。我對他的了解也隻是結束的時候調用一下。
除去上述方法之外,在ByteOutputStream内部還有ensureCapacity,grow,hugeCapacity這三個私有方法進行擴容。
-
I/O-位元組流-Piped
PipedInputStream和PipedOutputStream是管道位元組輸入流和管道位元組輸出流。這兩個類的名字與上兩個類中最大的差異就在于Piped。
管道位元組輸入/輸出流有一個明确的使用場景,就是在多線程之間用流的形式進行資料傳遞。假設有兩個線程Thread-1和Thread-2,這兩個線程之間通過流從Thread-1将資料傳遞給Thread-2,這種場景下, 需要在Thread-1中使用PipedOutputStream将Thread-1的資料寫入管道内,再在Thread-2中使用PipedInputStream從管道中将資料讀取出來。
PipedOutputStream
PipedOutputStream屬于java.io包,OutputStream的子類。
-
構造方法
在PipedOutputStream類中,提供了兩個構造方法。代碼如下:
/**
* Creates a piped output stream that is not yet connected to a
* piped input stream. It must be connected to a piped input stream,
* either by the receiver or the sender, before being used.
*
* @see java.io.PipedInputStream#connect(java.io.PipedOutputStream)
* @see java.io.PipedOutputStream#connect(java.io.PipedInputStream)
*/
public PipedOutputStream() {
}
第一個構造方法是無參構造方法,在方法注釋上可以看出來,這個方法隻是建立了一個PipedOutputStream對象,并且這個對象沒有連接配接到一個PipedInputStream上。
對于PipedOutputStream對象而言,在使用之前,必須連接配接到一個PipedInputStream對象上,不論是通過接受者或者發送者執行這個連結操作。
/**
* Creates a piped output stream connected to the specified piped
* input stream. Data bytes written to this stream will then be
* available as input from <code>snk</code>.
*
* @param snk The piped input stream to connect to.
* @exception IOException if an I/O error occurs.
*/
public PipedOutputStream(PipedInputStream snk) throws IOException {
connect(snk);
}
這個構造方法中傳入了一個PipedInputStream對象,在構造方法裡執行了connect()方法。這個方法就是在第一個構造方法的解釋裡提到的,連接配接的方法。
- 其他方法
-
connect(PipedInputStream): void
方法中傳入一個PipedInputStream對象,把傳入對象連接配接到PipedOutputStream上。這個連接配接的過程裡,有一個要求。連接配接的雙方不能都不能連接配接或者被連接配接到其他管道流上。
在PipedInputStream類中,也有一個connect方法,起到了同樣的作用。
在連接配接上之後,才可以進行寫操作,讀操作(PipedInputStream)。
- write(int): void
-
write(byte[],int,int): void,
這兩個方法都是重寫父類OutputStream中的方法,具體的用法和ByteArrayOutputStream中的一樣,這裡不進行贅述。
-
flush(): void
flush方法很重要。代碼如下
方法注解上寫的很明白,這個方法會強制流中所有的位元組寫出,并且通知關聯到這個管道的,等待讀取資料的readers(也就是之前連接配接上的PipedInputStream)Java基礎知識-03-C-2020-07-22Java基礎知識-03-C-2020-07-22 -
close(): void
在close方法裡,調用了關聯在這個輸出流上的PipedInputStream中的receivedLast()方法。這個方法的具體内容,會在下面的PipedInputStream中講解。
-
PipedInputStream
PipedInputStream,java.io包,InputStream類的子類。
-
構造方法
這個類中有四個構造方法。
/**
* Creates a <code>PipedInputStream</code> so
* that it is not yet {@linkplain #connect(java.io.PipedOutputStream)
* connected}.
* It must be {@linkplain java.io.PipedOutputStream#connect(
* java.io.PipedInputStream) connected} to a
* <code>PipedOutputStream</code> before being used.
*/
public PipedInputStream() {
initPipe(DEFAULT_PIPE_SIZE);
}
第一個構造方法,建立一個PipedInputStream對象,初始的循環緩沖區大小1024。
/**
* Creates a <code>PipedInputStream</code> so that it is not yet
* {@linkplain #connect(java.io.PipedOutputStream) connected} and
* uses the specified pipe size for the pipe's buffer.
* It must be {@linkplain java.io.PipedOutputStream#connect(
* java.io.PipedInputStream)
* connected} to a <code>PipedOutputStream</code> before being used.
*
* @param pipeSize the size of the pipe's buffer.
* @exception IllegalArgumentException if {@code pipeSize <= 0}.
* @since 1.6
*/
public PipedInputStream(int pipeSize) {
initPipe(pipeSize);
}
第二個構造方法,建立一個PipedInputStream對象,自定義了循環緩沖區的大小。
/**
* Creates a <code>PipedInputStream</code> so
* that it is connected to the piped output
* stream <code>src</code>. Data bytes written
* to <code>src</code> will then be available
* as input from this stream.
*
* @param src the stream to connect to.
* @exception IOException if an I/O error occurs.
*/
public PipedInputStream(PipedOutputStream src) throws IOException {
this(src, DEFAULT_PIPE_SIZE);
}
第三個方法,建立一個PipedInputStream對象,循環緩沖區大小1024,傳入一個PipedOutputStream對象,并且把這兩個對象進行連接配接。
/**
* Creates a <code>PipedInputStream</code> so that it is
* connected to the piped output stream
* <code>src</code> and uses the specified pipe size for
* the pipe's buffer.
* Data bytes written to <code>src</code> will then
* be available as input from this stream.
*
* @param src the stream to connect to.
* @param pipeSize the size of the pipe's buffer.
* @exception IOException if an I/O error occurs.
* @exception IllegalArgumentException if {@code pipeSize <= 0}.
* @since 1.6
*/
public PipedInputStream(PipedOutputStream src, int pipeSize)
throws IOException {
initPipe(pipeSize);
connect(src);
}
建立一個PipedInputStream,自定義它的循環緩沖區大小,并且傳入了PipedOutputStream對象,将這兩個管道進行連接配接。
- 其他方法
- connect(PipedOutputStream): void
- read():int
- read(byte[],int,int):int
-
available() :in
上面的方法與ByteArrayInputStream中的一樣,從輸入流中讀取資料,不進行贅述。
這裡有一個點是和先前講過的I/O流不同的地方。在之前介紹的内容裡,輸入和輸出流是獨立的存在的,但是管道流則不是。管道輸入輸出流二者必須作為一個整體才能使用。并且在資料傳輸中是通過三步走的形式,即:寫入->接收->讀取。下面的三個方法就是中間(接收環節)方法。
-
receive(int):void
一次往輸入流中讀取一個位元組。如果在讀取時,沒有任何一個可被讀取的位元組,目前方法會阻塞(線程的内容下篇會講)。
-
receive(byte[],int,int):void
和read(byte[],int,int)方法類似,隻是這個方法不會把接收長度當做傳回值進行傳回。這個方法是說,按照起始偏轉量,接收一段指定長度的位元組,并且把接收到的位元組放在傳入的byte[]中去。在沒有足夠多的輸入元素之前,這個方法會被阻塞。
-
receiveLast():void
這個方法,會在PipedOutputStream對象調用它的close()方法時被調用,表示已經把所有要的資料寫入到管道流對象中。并且,會喚醒等待的receive方法。
-
close(): void
這個方法表示接受者(PipedInputStream)主動進行關閉。
I/O-位元組流-Filter
在這篇部落格開頭講到過,除過按照位元組流字元流,輸入流輸出流之外,還有一個分類方式就是節點流和處理流。節點流是真實幹活的流,處理流是包裝在節點流上一層,給使用者提供更便利API的流。
這裡要講到的FilterInputStream就是這樣的一個處理流。
FIlterInputStream和FilterOutputStream裡包含了多個子類對象。其中最常用到的就是BufferdInputStream,BufferedOutputStream,DataInputStream和DataOutputStream。
順嘴一提,FilterInputStream,FilterOutputStream應用了裝飾者模式,後續寫設計模式的時候會詳細介紹這些内容。
在介紹具體的子類之前,先說一下這兩個父類,也就是FilterInputStream和FilterOutputStream。
這兩個父類中,最值得提到的就是構造方法,并且這種形式的構造方法在他們的子類中也得到了具體的展現。
FilterInputStream構造方法:
/**
* Creates a <code>FilterInputStream</code>
* by assigning the argument <code>in</code>
* to the field <code>this.in</code> so as
* to remember it for later use.
*
* @param in the underlying input stream, or <code>null</code> if
* this instance is to be created without an underlying stream.
*/
protected FilterInputStream(InputStream in) {
this.in = in;
}
在上面的構造方法裡可以看出來, 這個構造方法裡傳入了一個InputStream作為參數。這一點和之前提到的都不一樣。
FilterOutputStream構造方法:
/**
* Creates an output stream filter built on top of the specified
* underlying output stream.
*
* @param out the underlying output stream to be assigned to
* the field <tt>this.out</tt> for later use, or
* <code>null</code> if this instance is to be
* created without an underlying stream.
*/
public FilterOutputStream(OutputStream out) {
this.out = out;
}
同樣,在FilterOutputStream中也傳入了一個OutputStream對象作為參數。
I/O-位元組流-Filter-Buffered
在FilterInput/OutputStream中,包含多個常用子類,這裡先介紹第一對,BufferedInputStream和BufferedOutputStream。
BufferedInputStream
BufferedInputStream屬于java.io包下,是FilterInputStream類的子類。
-
構造方法
在BufferedInputStream中提供了兩個構造方法,具體代碼:
/**
* Creates a <code>BufferedInputStream</code>
* and saves its argument, the input stream
* <code>in</code>, for later use. An internal
* buffer array is created and stored in <code>buf</code>.
*
* @param in the underlying input stream.
*/
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
上面的構造方法裡傳入一個InputStream對象作為實際操作資料的對象。
/**
* Creates a <code>BufferedInputStream</code>
* with the specified buffer size,
* and saves its argument, the input stream
* <code>in</code>, for later use. An internal
* buffer array of length <code>size</code>
* is created and stored in <code>buf</code>.
*
* @param in the underlying input stream.
* @param size the buffer size.
* @exception IllegalArgumentException if {@code size <= 0}.
*/
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
這個構造方法裡不僅傳入了InputStream,還制定了緩存的大小。
- 其他方法
- available():int
- markSupported():boolrean
- mark(int):void
- read():int
- read(byte[],int,int):int
- reset():void
-
skpi(long):long
上述方法均是從父類中繼承來的,整體使用和意義不過多進行贅述。
這裡主要講一下兩個read中的内部過程。
對這個方法進行講解的時候,會結合ByteArrayInputStream進行對比講解。
對象類 ByteArrayInputStream BufferedInputStream 是否需要裝載其他InputStream對象 否 是 read()内部執行過程 這個過程裡使用到了ByteArrayInputStream中的三個成員變量。如果目前位置(pos)<總數(count),就傳回buf[](初始化時傳入的)的下一個元素。如果>=總數,則傳回-1 這個過程裡使用到了BufferedInputStream中的三個成員變量。如果目前位置(pos)>=結尾(count),調用BufferedInputStream中的fill()方法,調用之後再次對pos和count進行比對。如果同樣是pos>=count,則傳回-1.如果沒有超過,則調用内部方法getBufIfOpen():byte[]傳回buf中的下一個元素 read(byte[],int.int)内部執行過程 同樣先是比較目前位置和總數的關系。如果pos>=count,傳回-1.如果不是,用count-pos的結果與目前方法的第三個入參去最小值。調用System.arraycoyp()方法,将ByteArrayInputStream中的buf[]按照起始位置,偏移量,讀取長度這三個值拷貝到目前方法的第一個入參(byte[])中,并将copy的長度進行傳回 這個方法中實際上調用到了BufferedInputStream中的四個私有方法,分别是fill()加載資料,getBufIfOpen()傳回buf[]對象,getInIfOpen()傳回InputStream對象,read1(byte[],int,int)從流中讀取資料。這些方法内部執行邏輯詳情推薦自己看内部執行過程,整個過程不太好用原因說清楚 -
fill():void
核心方法
簡單而言這個方法是給緩沖區裡加載資料用的。
内部邏輯比較長,不友善用語言描述整體思想。主要講,就是給緩沖區加載資料,并且在這個位置中把pos(目前位置),count(總數),markpos(調用mark方法時的目前位置pos)marklimit(調用mark時的入參)進行了一連串的指派和比較。
-
getBufIfOpen():byte[]
從方法名可以明白這個方法的意思,get Buf If Open,判斷目前對象内的成員變量buf[]是否為null,如果是null抛出IOException,如果不是,傳回buf[]。這個buf[],是在構造方法中的InputStream的buf[]。
-
getInIfOpen():InputStream
判斷構造方法中傳入的InputStream是否為null,如果是,則抛出IOException,否則傳回InputStream
-
read1(byte[],int,int):int
核心方法
從read1方法的入參就可以猜測到,在調用read(byte[],int,int)方法的内部是調用了read1方法。
在read1方法内部,根據總數(count),目前位置(pos),要讀取的長度(這個方法的第三個入參)進行各類比較,并且在有必要的時候執行fill()方法加載資料,最終将資料資料從輸入流裡寫入到第一個入參裡。
綜述,使用BufferedInputStream會帶來最大的好處:
提升效率。由于緩沖區的存在,是以在使用BufferedInputStream的時候會比直接使用節點流的效率更高。
執行個體代碼如下:
package com.phl.test;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class IOTest {
//替換成自己的檔案位址即可
private final String path = "file_path";
public IOTest() {
}
public void readByBuffer(){
try {
FileInputStream fileInputStream = new FileInputStream(new File(this.path));
//不指定緩沖區大小的話,預設大小是8192
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
byte[] bytes = new byte[1024];
//緩沖區大小8192,每次讀取大小1024,也就是說我讀8次,才會執行一次IO
while (bufferedInputStream.read(bytes) != -1){ }
bufferedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public void readByInput(){
try {
FileInputStream fileInputStream = new FileInputStream(new File(this.path));
byte[] bytes = new byte[1024];
//每次讀取1024,因為沒有緩沖區存在,是以每讀一個1024,我就會執行一次IO
while (fileInputStream.read(bytes) != -1){ }
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
IOTest ioTest = new IOTest();
long startTime1 = System.currentTimeMillis();
ioTest.readByBuffer();
long endTime1 = System.currentTimeMillis();
System.out.println(endTime1 - startTime1);
long startTime2 = System.currentTimeMillis();
ioTest.readByInput();
long endTime2 = System.currentTimeMillis();
System.out.println(endTime2 - startTime2);
}
}
核心點解釋:
- 上方兩種讀取操作過程,BufferedInputStream是IO->緩沖區->讀取塊,FileInputStream是IO->讀取塊。
- 從上面的邏輯來看,當我的緩沖區遠大于讀取塊時,會明顯降低IO開銷,同時由此産生的緩沖區->讀取塊的時間消耗(從記憶體A往記憶體B讀取)可以忽略不計。但是如果緩沖區比讀取塊大不了多少(如緩沖區1024,讀取塊1000),那麼不僅不能明顯的降低IO開銷,同時比起直接用InputStream而言,增加了一次緩沖區->讀取塊的過程,就得不償失了。
- 針對上面的例子,你可以更改path成你電腦中的某個歌曲,某個電影的位址,并且更改緩沖區,讀取塊的參數值,對多次結果進行觀察,就會證明我上述結論。
BufferedOutputStream
BufferedOutputStream與BufferedInputStream類似,都是内部加載了一個真實處理資料的節點流,自己本身作為處理流存在。
在BufferedOutputStream中,同樣提供了兩個構造方法。
/**
* Creates a new buffered output stream to write data to the
* specified underlying output stream.
*
* @param out the underlying output stream.
*/
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}
上面的構造方法中,不指定緩沖區大小,預設8192,下面的構造方法會指定緩沖區大小。
/**
* Creates a new buffered output stream to write data to the
* specified underlying output stream with the specified buffer
* size.
*
* @param out the underlying output stream.
* @param size the buffer size.
* @exception IllegalArgumentException if size <= 0.
*/
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
其他方法不過多進行贅述,通過之前的BufferedInputStream中就可以看出來,在使用這些處理流的時候,基礎API使用都是一個路數,畢竟他們都直接或者間接繼承自InputStream或者OutputStream。真正核心問題往往是處理流内部的private方法。
-
flushBuffer()
這個方法内部,調用了節點流的write(byte[],int,int)方法。flushBuffer()方法本身會在調用處理流的write(int),write(buf[],int,int),flush()方法時,在這些方法内部被調用。
同樣,在輸出流的使用時,使用帶緩沖區的流比直接使用節點流的效率更高,測試例子可以仿照上面讀取的例子進行編寫。
I/O-位元組流-Filter-Data
作為另一個處理流,DataInputStream和DataOutputStream的構造方法中都需要傳入一個輸入\輸出流對象作為入參。
DataInputStream和DataOutputStream最大的特點在于,這兩個類分别實作了接口DataInput和DataOutput。
注意,DataInputStream和DataOutputStream需要一起使用。下面就介紹為什麼需要一起使用。
作為處理流,這兩個流對象提供了他們所特有的方法。
DataInputStream方法如下:
通過截圖可以直覺的發現,在DataInputStream中沒有重寫read(),read(byte[],int,int)這兩個核心讀取資料的方法,而是在類中通過實作DataInput接口中的方法來進行資料讀取,同時還有自己定義的readUTF()方法進行資料讀取。
接口中的方法,引用readInt()為例,對整體接口方法進行解讀。在readInt()代碼如下:
/**
* See the general contract of the <code>readInt</code>
* method of <code>DataInput</code>.
* <p>
* Bytes
* for this operation are read from the contained
* input stream.
*
* @return the next four bytes of this input stream, interpreted as an
* <code>int</code>.
* @exception EOFException if this input stream reaches the end before
* reading four bytes.
* @exception IOException the stream has been closed and the contained
* input stream does not support reading after close, or
* another I/O error occurs.
* @see java.io.FilterInputStream#in
*/
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));
}
在readInt()方法裡,因為int類型資料占4個位元組,是以在readInt()方法中,通過節點流執行了4次read()方法,并把執行結果拼接成最終的int類型資料進行傳回。
至于每個讀取,以及後續會說的寫入方法裡,針對不同類型到底是多少位元組,這裡不過多讨論,感興趣可以通過源碼進行了解。
除過readInt()這種,直接通過節點流執行read()方法之外,比如readLong()方法内部,通過調用同樣是實作自接口的readFully(byte[],int,int)方法,而在readFully()中,則是通過節點流調用read(byte[],int,int)的形式完成資料讀取。
綜上,可以看到在DataInputStream中,通過讀取定長的位元組,以及對讀取結果的拼接和轉換,繼而實作了從輸入流中直接讀取基礎資料類型的功能。而先前提到的==readUTF()==方法就更不可思議了,會從輸入流中讀取到String類型。
DataOutputStream方法如下:
通過上圖可以發現,DataOutputStream類中與DataInputStream類似,實作了很多對基礎資料類型以及String資料類型的寫入方法。
對于其他的節點流而言,想要精準的控制輸入或者輸出結果的類型,都需要把要寫入或者讀取的數字轉byte[],才能執行。但是在使用DataInputStream和DataOutputStream的過程裡,這個過程很明顯被簡化了,操作更順暢。
現在說,為什麼這兩個要一起使用?
與其說要一起使用,更應該說是非常推薦一起使用,不論是在輸入還是輸出的過程中,因為實際幹活的都是節點流,是以使用節點流同樣可以完成現在的過程。但是,明顯在使用這樣的處理裡極大的降低了計算位元組length與資料類型關系時出現的錯誤。
I/O-位元組流-Object
ObjectInputStream和ObjectOutputStream是操作對象的流。這裡就得提到一個知識點,序列化和反序列化。我們都知道在對Java對象進行序列化和反序列化的時候,需要實作Serializable接口或者Externalizable接口。我一般都會選擇Serializable接口,這個接口使用起來更簡單,并且正常的序列化功能就能滿足我的場景。
在實作了Serializable接口之後,還需要給目前類指定一個serialVersionUID,不論這個serialVersionUID是生成的也好,自己寫的1L也罷,這個不是必須有的,但是如果不寫,所有的IDE都會有個提示,比較鬧心。同時,如果這個類中的部分屬性不要進行序列化,那麼需要給這個屬性前面加上transient關鍵字,這樣在反序列化的時候,有被transient關鍵字修飾的屬性,如果是基礎資料類型,那對應的值就是預設值,如果不是基礎資料類型,那對應的就是null。這個是對序列化和反序列化最基礎的了解了。
當做完上述内容,就滿足了序列化和反序列化的基礎條件。在進行序列化和反序列化的操作是,就會用到上述提到的兩個流對象:ObjectInputStream和ObjectOutputStream。其中,序列化的時候是把對象寫到某個地方,那對應的ObjectOutputStream就是序列化流,反之,ObjectInputStream就是反序列化流。
ObjectInputStream和ObjectOutputStream源碼内部很是複雜,這裡采用簡單的語言進行描述,如有興趣,可通過閱讀源碼進行了解。
舉一個最常見的例子,我現在需要通過檔案進行序列化和反序列化。
-
序列化過程
在進行序列化時,首先我需要建立一個節點流來幹活,因為是通過檔案進行序列化,那我可以建立一個FileOutputStream作為節點流,再建立一個ObjectOutputStream(處理流)進行操作(節點流對象是這個構造方法的入參)。假設要傳遞的對象是A,那麼我可以通過調用ObjectOutputStream對象的writeObject(A a)方法實作序列化。執行返程後,在FileOutputStream指定的位置就會有我的序列化檔案。
-
反序列化過程
在進行反序列化時,因為場景中說的是通過檔案,是以我要建立一個FileInputStream作為節點流,同時建立一個ObjectInputStream(處理流)進行操作(節點流對象是這個構造方法的入參)。假設要進行反序列化的對象是A,那麼就可以調用ObjectInputStream對象的readObject()方法,并對傳回的結果進行強制類型轉換,就能得到反序列化的A對象。
當然,上述整個過程是一個非常簡單和入門的操作,隻能當個demo樂呵樂呵。更難的使用方法,不打算在基礎知識這個分欄下寫,當以後寫Java進階應用的時候,會對這部分内容進行拔高。
I/O-位元組流-File
之是以把FileInputStream和FileOutputStream放在最後講,因為這兩個流對象在日常使用中最為頻繁,網上各類資料,各種寫法應該也是最多的。放在這裡是希望通過上面多個位元組流的講解,讓大家對位元組流有了一個更深的了解後再去看這裡的内容,希望得到的效果就是讓大家覺得FileInputStream和FileOutputStream本身并不神秘。
在将這部分内容的時候,我打算通過以下的形式将,而不是像上面一樣常用方法都分開去寫。因為當大家耐着心思從上面一路讀下來之後,我這種寫法隻會讓你覺得清爽,省事兒。
-
FileInputStream
FileInputStream内部結構圖
Java基礎知識-03-C-2020-07-22Java基礎知識-03-C-2020-07-22 上圖中用三種不同顔色的框進行了分類。
紅色框,重寫了自InputStream的方法和抽象方法,這些方法就是我們平時使用時經常要用的API。因為是繼承自InputStream類,紅色框裡的方法與上面寫道的每個InputStream的子類都是大同小異,不過多進行贅述。
藍色框,這個是整個類中唯一需要留心的地方。類似于BufferedInputStream,他在使用的時候也是直接調用InputStream提供的方法,但是在那些方法内部都在調用自己提供的類似fill(),getInIfOpen(),getBufIfOpen()這些方法來完成内部邏輯。在FileInputStream中也是如此,紅色框内的方法在具體實作的過程裡,調用了藍色框内的方法(除了initDs()這是一個native方法,寫在靜态代碼塊裡,這個是JVM層面的東西,這裡不提了)。
綠色框,構造方法。
-
FileOutputStream
FileOutputStream内部結構圖
與FileInputStream類内部驚人的相似。
紅色框,繼承自InputStream類的方法,也是我們最常用的API。
藍色框,紅色框中的方法的内部調用了藍色框裡的方法。
綠色框,構造方法。
至此,這篇I/O的部落格中,位元組流的部分已經講完了,寫下來的會是字元流。
開始之前,說一個承上啟下的話。位元組流和字元流從字面來看,一字之差,這也說明了字元流隻能處理純文字的内容。而對于計算機資料來說,不論是什麼内容,什麼類型,什麼字尾,都可以轉化成位元組數組的形式存在,是以對于任意一個要用流去處理的資料而言,使用位元組流都不會出錯。隻是,在應對純文字内容是,字元流有更便捷的使用體驗。
前面介紹字元流的時候,那些類都是java.io包下,直接或者間接繼承自InputStream或者OutputStream。
下面的字元流,這些類都是java.io包下,直接或者間接繼承自Reader或者Writer。
I/O-字元流-CharArray
CharArrayReader和CharArrayWriter,從名字就能看出來,這個是應對字元的,并且是Char[]。注意了,注意了,在正文一開始的一個類是ByteArray,現在這裡是CharArray是以他們的用法,了解上都是非常的相似。
最為字元流整體章節中的第一個小章節,我會把這裡的每個方法都仔細寫一遍,但是剩下的幾個類就隻是挑應知應會去寫。
CharArrayReader
CharArrayReader類,是java.io包下,繼承自Reader類。在CharArrayReader類中,提供了兩個構造方法。
/**
* Creates a CharArrayReader from the specified array of chars.
* @param buf Input buffer (not copied)
*/
public CharArrayReader(char buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
建立一個CharArrayReader對象,入參是要進行讀取的char[],并且将CharArrayReader對象的開始位置設成0,總數設成入參的length
/**
* Creates a CharArrayReader from the specified array of chars.
*
* <p> The resulting reader will start reading at the given
* <tt>offset</tt>. The total number of <tt>char</tt> values that can be
* read from this reader will be either <tt>length</tt> or
* <tt>buf.length-offset</tt>, whichever is smaller.
*
* @throws IllegalArgumentException
* If <tt>offset</tt> is negative or greater than
* <tt>buf.length</tt>, or if <tt>length</tt> is negative, or if
* the sum of these two values is negative.
*
* @param buf Input buffer (not copied)
* @param offset Offset of the first char to read
* @param length Number of chars to read
*/
public CharArrayReader(char buf[], int offset, int length) {
if ((offset < 0) || (offset > buf.length) || (length < 0) ||
((offset + length) < 0)) {
throw new IllegalArgumentException();
}
this.buf = buf;
this.pos = offset;
this.count = Math.min(offset + length, buf.length);
this.markedPos = offset;
}
建立一個CharArrayReader對象,有三個參數。
第一個參數是要讀取的char[].
第二個參數是起始偏移量。
第三個參數是(偏移量+長度)與要讀取的char[]長度之間的最小值,并且把markedPos設定成偏移量。
除去上述的構造方法,CharArrayReader類中還有如下方法
-
ensureOpen():void
這個方法在CharArrayReader中除了close()方法之外的其他方法裡都被調用過。
這個方法内部是判斷目前對象中的buf(初始化時傳入的char[])是否為null。如果是null,會彈出IO異常,如果不是null,沒有任何操作。這個方法的意義是在進行其他真實資料操作之前,先确認資料是存在的。
-
ready():boolean
傳回boolrean值,true辨別目前流對象可以進行讀取,false表示不能讀取。這個方法是個線程同步方法,對于CharArrayReader而言,這個對象永遠可以被讀取。但是,如果已經把所有内容都讀取完了,則會傳回false。
-
read():int
從buf中根據目前pos讀取下一個元素。如果已經到了末尾,會傳回-1作為結束辨別。如果不是,則傳回對應的元素(這裡雖然是char,但是傳回了int,是以要傳回結果的時候,需要進行轉換)。
-
read(char[],int,int):int
這個方法是把CharArrayReader中的内容讀取到一個目标char[]中去。這個方法有三個參數。
第一個參數,char[],用來接收從CharArrayReader中讀取出來的内容。
第二個參數,起始偏移量。
第三個參數,要讀取的長度。如果CharArrayReader裡的buf剩餘内容不夠讀取的長度,則隻讀取剩餘的長度。
這個方法的傳回值是每一次讀取的長度。
-
markSupported():boolean
這個方法表示CharArrayReader類是否支援mark和reset方法。永遠傳回 true。
-
mark(int):void
mark方法,之前很多流對象的mark方法,都是把傳入的參數設成pos,把目前位置設成markpos,但是在這裡卻不一樣。源碼的注解上寫,因為這個流對象中傳入的是一個字元數組,沒有實際的限制,是以傳入的參數被忽略。代碼如下:
/**
* Marks the present position in the stream. Subsequent calls to reset()
* will reposition the stream to this point.
*
* @param readAheadLimit Limit on the number of characters that may be
* read while still preserving the mark. Because
* the stream's input comes from a character array,
* there is no actual limit; hence this argument is
* ignored.
*
* @exception IOException If an I/O error occurs
*/
public void mark(int readAheadLimit) throws IOException {
synchronized (lock) {
ensureOpen();
markedPos = pos;
}
}
真正做的事情隻是記錄下了在這裡執行了mark操作,而沒有理會傳入的參數。
-
reset():void
把目前位置設定成最近一次執行mark的位置。如果沒有,會把目前位置設定成最開始的位置。
-
skip(long):long
跳過指定長度的元素,傳回值是真實跳過的元素。因為可能buf中剩餘的元素小于要跳過的元素個數。
-
close():void
這個close方法裡面将buf設定成了null。
CharArrayWriter
CharArrayWriter類,是java.io包下,繼承自Writer類。
CharArrayWriter類中提供了兩個構造方法。
/**
* Creates a new CharArrayWriter.
*/
public CharArrayWriter() {
this(32);
}
建立CharArrayWriter對象,并給一個預設的緩沖器,大小為32.
/**
* Creates a new CharArrayWriter with the specified initial size.
*
* @param initialSize an int specifying the initial buffer size.
* @exception IllegalArgumentException if initialSize is negative
*/
public CharArrayWriter(int initialSize) {
if (initialSize < 0) {
throw new IllegalArgumentException("Negative initial size: "
+ initialSize);
}
buf = new char[initialSize];
}
建立一個CharArrayWriter對象,自定義了緩沖區大小。
CharArrayWriter類中提供了如下方法
-
append(char):CharArrayWriter
方法内部調用了write(int)
-
append(CharSequence):CharArrayWriter
方法内部調用了write(String,int,int),其中第二個參數off是0,第三個參數是CharSequence的長度。
-
append(CharArrayWriter,int,int):CharArrayWriter
方法内容調用了write(String,int,int)
-
write(int):void
給CharArrayWriter對象裡的buf後面續上傳入的參數(強轉成char)
-
write(char[],int,int):void
給CharArrayWriter對象裡的buf後面續上傳入的參數。
這個方法中有三個參數。
第一個參數,char[],要寫入到buf中的内容。
第二個參數,int,要從char[]的那個位置開始往buf裡寫。
第三個參數,int,從開始的位置往buf裡面總共寫多少個字元。
-
write(String,int,int):void
和上一個方法一樣,隻是第一個參數從char[]換成了String類型。
-
toCharArray():char[]
把buf中的内容以char[]的格式進行傳回。
-
wtireTo(Writer):void
把CharArrayWriter中buf裡的内容寫到傳入的Writer中。
-
flush():void
沖刷資料
-
reset():void
重置緩沖區,以便您可以再次使用它,而不會丢棄已配置設定的緩沖區。
-
size():int
傳回buf中的元素個數。
-
close():void
關閉。
I/O-字元流-Piped
PipedReader,PipedWriter用于在不同線程之間通過字元流傳遞資料。他們都是java.io包下,分别繼承自Reader和Writer類。PipedReader與PipedWriter這一組和PipedInputStream與PipedOutputStream非常類似。PipedReader和PipedWriter需要組隊使用。
PipedWriter類内部結構如下。
在PipedWriter類中,提供了兩個構造方法。
無參構造方法會建立一個沒有連接配接到PipedReader的PipedWriter對象。
有參構造方法中會傳入一個PipedReader,建立的PipedWriter會與入參的PipedReader連接配接到一起。
在寫入之前,必須確定目前的Writer已經和一個Reader連接配接在一起,手動連接配接通過connect(PipedReader)方法。
連接配接之後,通過write(int),write(char[],int,int)方法進行資料寫入。
flush()方法進行沖刷資料,close()方法關閉輸出流。
PipedReader類内部結構如下。
同樣都是管道流的形式,是以在管到字元流的寫入和讀取過程裡,與上面提到的管道位元組流的順序一樣,寫入->接收->讀取,三步走。
在PipedReader中有四個構造方法,這四個構造方法用來建立PipedReader對象。在構造方法了可以指定接收緩沖區的大小,要進行連接配接的PipedWriter對象。
這個類中的方法與Piped位元組流中的非常類似,詳情見上面的Piped位元組流章節。
I/O-字元流-Filter
FilterReader和FilterWriter隻是一個非常簡單,且,在我實際應用裡也非常用不太到的處理流,切記,這兩個是處理流。唯一需要注意的,處理流在初始化的時候,需要傳入節點流。這兩個初始化的時候,分别需要傳入Reader對象和Writer對象。
I/O-字元流-Buffered
BufferedReader和BufferedWriter在日常使用中很頻繁。首先,這兩個是處理流,是以在建立的時候需要傳入節點流作為入參。
BufferedReader類中最常用個方法是readLine():String,一次讀取一行文本。
對應的,BufferedWriter中最常用的方法是writeLine(String,int,int):void
這兩個類在網上的解釋,用法,demo一搜一大把,并且這兩個類與上面的BufferedInputStream和BufferedOutputStream非常類似,這裡就過多介紹。不論是從其他地方查,或者是根據上面的BufferedInputStream,BufferedOutputStream進行類比,都可以得到詳實的解釋。
I/O-字元流-InputStreamReader/OutputStreamWriter
InputStreamReader和OutputStreamWriter是處理流。
單單把這兩個處理流标黃,是因為這兩個處理流做了一個很厲害的事情。
從最開始洋洋灑灑寫了這麼多,文章的标題都是位元組流,字元流這樣進行分類,并且中間也說了,位元組流可以處理所有情況,字元流隻能處理文本内容。那麼,既然位元組流可以應對所有的情況,那怎麼才能把一個位元組流,轉成一個字元流?
InputStreamReader,能把InputStream包裝成Reader。
OutputStreamWriter,能把OutputStream包裝成Writer。
這是這兩個類最大,最厲害,最需要記憶的地方。其他沒啥。
I/O-字元流-File
FileReader,FileWriter用來寫入寫出檔案的字元流。
沒太多值得介紹的,網上的東西太多,并且他的用法也與上面講過的FileInputStream和FileOutputStream非常類似。這裡不過多進行贅述。
I/O-圖上有兩個沒講的在這裡
PrintStream,處理流,我們經常寫的sout,裡面用到的就是PrintStream對象。
這個對象很厲害,有很多列印的方法,并且可以列印到很多地方。
PrintWriter,處理流,構造方法裡可以傳Writer和OutputStream對象。用法也很廣泛。
綜述,上面講了很多種IO對象,他們普遍成對出現,位元組字元也成對出現。内部的結構,思想都很相似,很推薦去看看源碼,這樣就非常好了解IO是什麼了。