系統學習 Java IO (一)----InputStream & OutputStream
InputStream
是Java IO API中所有輸入流的父類。
表示有序的位元組流,換句話說,可以將 InputStream 中的資料作為有序的位元組序列讀取。
這在從檔案讀取資料或通過網路接收時非常有用。
InputStream 通常連線到某些資料來源,如檔案,網路連線,管道等
看如下程式碼片段:
public class InputStreamExample { public static void main(String[] args) throws IOException { InputStream inputStream = new FileInputStream("D:\\out.txt"); //do something with data... int data = inputStream.read(); while (data != -1) { System.out.print((char) data); data = inputStream.read(); } inputStream.close(); } }
注意:為了程式碼清晰,這裡並沒有考慮處理異常的情況,IO 異常處理有專門的介紹。
read()
此方法返回的是 int 值,其中包含讀取的位元組的位元組值,可以將返回的 int 強制轉換為 char 輸出。
如果 read() 方法返回 -1 ,則表示已到達流的末尾,這意味著在 InputStream 中不再有要讀取的資料。
也就是說,-1 作為 int 值,而不是 -1 作為位元組或短值,這裡有區別!
InputStream 類還包含兩個 read() 方法,這些方法可以將 InputStream 源中的資料讀入位元組陣列。
這些方法是:
- int read(byte[]);
- int read(byte[], int offset, int length);
一次讀取一個位元組數比一次讀取一個位元組要快得多,所以在可以的時候,使用這些讀取方法而不是 read() 方法。
read(byte [])方法將嘗試將盡可能多的位元組讀入作為引數給出的位元組陣列,因為陣列具有空間。
該方法返回一個 int ,其值是實際讀取了多少位元組,這點和 read() 方法不一樣。
如果可以從 InputStream 讀取的位元組少於位元組陣列的空間,則位元組陣列的其餘部分將包含與讀取開始之前相同的資料。例如:
InputStream input = new ByteArrayInputStream("123456789".getBytes()); byte[] bytes = new byte[4]; // 每次只讀取 4 個位元組 int data = input.read(bytes); while (data != -1) { System.out.print(new String(bytes)); data = input.read(bytes); }
將輸出 123456789678 ,而不是預期的 123456789 !
因為第一次讀取進 bytes 是 1234 ,第二次將是 5678 ,現在只剩下 9 一個數字了,注意此時 bytes 的值是 5678 ,然後再讀取剩下 1個 9,不能裝滿 bytes 了,只能覆蓋 bytes的第一個位元組,最後返回的bytes 是 9678。
所以記住檢查返回的 int 以檢視實際讀入位元組陣列的位元組數。
int read(byte[], int offset, int length);方法和 read(byte [])方法差不多,只是增加了偏移量和指定長度。
和 read() 一樣,都是返回 -1 表示資料讀取結束。
使用例項如下:
InputStream inputstream = new FileInputStream("D://out.txt");
byte[] data = new byte[1024];
int bytesRead = inputstream.read(data);
while(bytesRead != -1) {
doSomethingWithData(data, bytesRead);
bytesRead = inputstream.read(data);
}
inputstream.close();
首先,此示例建立一個位元組陣列。
然後它建立一個名為 bytesRead 的 int 變數來儲存每次讀取 byte [] 呼叫時讀取的位元組數,
並立即分配 bytesRead 從第一次讀取 byte [] 呼叫返回的值。
mark() and reset()
InputStream 類有兩個名為 mark() 和 reset() 的方法,InputStream 的子類可能支援也可能不支援:
- 該子類覆蓋 markSupported() 並返回true,則支援 mark( )和 reset() 方法。
- 該子類覆蓋 markSupported() 並返回 false ,則不支援 mark() 和 reset() 。
- 該子類不重寫 markSupported() 方法 ,則是父類的預設實現
public boolean markSupported() { return false; }
也是不支援 mark( )和 reset() 方法
mark() 在 InputStream 內部設定一個標記,預設值在位置 0 處。
可以手動標記到目前為止已讀取資料的流中的點,然後,程式碼可以繼續從 InputStream 中讀取資料。
如果想要返回到設定標記的流中的點,在 InputStream 上呼叫 reset() ,然後 InputStream “倒退”並返回標記,
如此,便可再次從該mark點開始返回(讀取)資料。很明顯這可能會導致一些資料從 InputStream 返回多次。我來舉個例子:
public static void testMarkAndReset() throws IOException {
InputStream input = new ByteArrayInputStream("123456789".getBytes());
System.out.println("第一次列印:");
int count = 0;// 計算是第幾次讀取,將在第二次讀取時做標記;
byte[] bytes = new byte[3]; // 每次只讀取 3 個位元組
int data = input.read(bytes);
while (data != -1) {
System.out.print(new String(bytes));
if (++count == 2) { // 在第二輪讀取,即讀到數字 4 的時候,做標記
input.mark(16); // 從 mark 點開始再過 readlimit 個位元組,mark 將失效
}
data = input.read(bytes);
}
input.reset();
System.out.println("\n在經過 mark 和 reset 之後從 mark 位置開始列印:");
data = input.read(bytes);
while (data != -1) {
System.out.print(new String(bytes));
data = input.read(bytes);
}
}
將會輸出:
第一次列印:
123456789
在經過 mark 和 reset 之後從 mark 位置開始列印:
789
另外要說明一下 mark(int readlimit) 引數,readlimit 是告訴系統,過了這個 mark 點之後,給本宮記住往後的 readlimit 個位元組,因為到時候 reset 之後,要從 mark 點開始讀取的;但實際情況和 jdk 文件有出入,很多情況下呼叫 mark(int readlimit) 方法後,即使讀取超過 readlimit 位元組的資料,mark 標記仍有效,這又是為什麼呢?網上有人解答,但我還是決定親自探索一番。
我們這個例項引用的實際物件是 ByteArrayInputStream
先看一下它的原始碼:
/* Note: The readAheadLimit for this class has no meaning.*/
public void mark(int readAheadLimit) {
mark = pos;
}
好傢伙,它說這個引數對於這個類沒有任何作用。
注意:這段是原始碼分析可看可不看,跳過不影響閱讀
那我們在看看其他的 InputStream 子類,經驗證,FileInputStream 和一些實現類不支援 mark() 方法,我們看看 BufferedInputStream
類原始碼:
我先把一些欄位的含義說明一下:
count
索引1大於緩衝區中最後一個有效位元組的索引。 該值始終在0到buf.length的範圍內; 元素buf [0]到buf [count-1]包含從底層輸入流獲得的緩衝輸入資料。在 read() 方法中讀完資料返回 -1 就是因為if (pos >= count) return -1;
pos
指緩衝區中的當前位置。 這是要從 buf 陣列中讀取的下一個字元的索引。
該值始終在 0 到 count 範圍內。 如果它小於 count,則 buf [pos] 是要作為輸入提供的下一個位元組; 如果它等於 count ,則下一個讀取或跳過操作將需要從包含的輸入流中讀取更多位元組。(這句話不理解沒關係)markpos
是呼叫最後一個 mark() 方法時 pos 欄位的值。該值始終在-1到pos的範圍內。 如果輸入流中沒有標記位置,則此欄位為-1。
- BufferedInputStream 是每次讀取一定量的資料到 buf 陣列中的,設定了 readlimit 肯定是想讓陣列從 mark 索引開始至少記錄 mark + readlimit 索引。
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;
}
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos < 0)
pos = 0; /* 沒有標記就直接丟掉快取,使用buffer取新資料 */
else if (pos >= buffer.length) /* 緩衝區中當前位置比buffer陣列大,才執行下面程式碼 */
if (markpos > 0) { /* 可以把 markpos 左邊的資料丟掉 */
int sz = pos - markpos; // 需要快取的位元組長度,從 markpos 開始
System.arraycopy(buffer, markpos, buffer, 0, sz); // 複用記憶體空間
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) { // 如果 buffer 的長度已經大於 marklimit
markpos = -1; /* 那 mark 就失效了*/
pos = 0; /* 刪除buffer內容,取新資料 */
} else if (buffer.length >= MAX_BUFFER_SIZE) { // 如果buffer過長就拋錯
throw new OutOfMemoryError("Required array size too large");
} else { /* buffer 還沒 marklimit 大,擴容到 pos 的2倍或者最大值 */
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;
if (nsz > marklimit)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
可以得出:設定標記後,
- 如果緩衝區中當前位置比 buffer 陣列小,也就是還沒讀完 buffer 陣列,那 mark 標記不會失效;
- 下次繼續讀取,超過 buffer 大小位元組後,判斷 markpos 是否大於0,如果 markpos 大於0,即還在 buffer 陣列內,則把 markpos 左邊的資料清除,markpos 指向 0 , 複用記憶體空間,並設定 buffer 的大小為 (pos - markpos) 的值;
- 再繼續讀取,此時 markpos 肯定不在 buffer 陣列包含範圍了,此時判斷 buffer 的長度是否大於等於
marklimit ,如果小於 marklimit ,那說明設定 mark 後讀取的資料長度還沒達到要求的 marklimit 了,給我繼續,保持從 mark 點開始快取, mark 標記不會失效。然後 buffer 就擴容到Math.min(2倍 pos 或最大值 ,marklimit); - 再繼續讀取,同上,buffer 這麼努力擴容,總有大於 marklimit 的時候,這時說明設定 mark 後繼續讀取的資料長度已經超過要求的 marklimit 了,仁盡義至,標記失效;
我們就只分析了 ByteArrayInputStream 和 BufferedInputSteam 類的演算法,其它輸入流不知道。因此 mark() 方法標記時,務必考慮好 readlimit 的值。
OutputStream
OutputStream通常始終連線到某個資料目標,如檔案,網路連線,管道等。 OutputStream的資料目標是寫入OutputStream的所有資料最終結束的地方。
write(byte)
write(byte)方法用於將單個位元組寫入 OutputStream。 OutputStream 的 write() 方法接受一個 int ,其中包含要寫入的位元組的位元組值。 只寫入 int 值的第一個位元組。 其餘的被忽略了。
OutputStream 的子類可以有替代的 write() 方法。 例如,DataOutputStream允許您使用相應的方法writeBoolean(),writeDouble() 等編寫諸如 int,long,float,double,boolean 等 Java 型別。
write(byte[] bytes) , write(byte[] bytes, int offset, int length)
和 InputStream 一樣,它們也可以將一個數組或一部分位元組寫入 OutputStream 。
flush()
OutputStream 的flush() 方法將寫入 OutputStream 的所有資料重新整理到底層資料目標。 例如,如果 OutputStream 是 FileOutputStream ,則寫入 FileOutputStream 的位元組可能尚未完全寫入磁碟。 即使您的Java程式碼已將其寫入 FileOutputStream ,資料也可能在某處快取在記憶體中。 通過呼叫 flush() ,您可以確保將任何緩衝的資料重新整理(寫入)到磁碟(或網路,或 OutputStream 的目標所具有的任何其他內容)。