通過mark和reset方法重複利用InputStream
阿新 • • 發佈:2019-02-03
在上篇文章中我們已經簡單的知道可以通過快取InputStream來重複利用一個InputStream,但是這種方式的缺點也是明顯的,就是要快取一整個InputStream記憶體壓力可能是比較大的。如果第一次讀取InputStream是用來判斷檔案流型別,檔案編碼等用的,往往不需要所有的InputStream的資料,或許只需要前n個位元組,這樣一來,快取一整個InputStream實際上也是一種浪費。
其實InputStream本身提供了三個介面:
第一個,InputStream是否支援mark,預設不支援。
第一個介面很簡單,就是標明該InputStream是否支援mark。
mark介面的官方文件解釋:
“在此輸入流中標記當前的位置。對 reset 方法的後續呼叫會在最後標記的位置重新定位此流,以便後續讀取重新讀取相同的位元組。
readlimit 引數告知此輸入流在標記位置失效之前允許讀取許多位元組。
mark 的常規協定是:如果方法 markSupported 返回 true,則輸入流總會在呼叫 mark 之後記住所有讀取的位元組,並且無論何時呼叫方法 reset ,都會準備再次提供那些相同的位元組。但是,如果在呼叫 reset 之前可以從流中讀取多於 readlimit 的位元組,則根本不需要該流記住任何資料。”
reset介面的官方文件解釋:
將此流重新定位到對此輸入流最後呼叫 mark 方法時的位置。
reset 的常規協定是:
如果方法 markSupported 返回 true,則:
如果建立流以來未呼叫方法 mark,或最後呼叫 mark 以來從該流讀取的位元組數大於最後呼叫 mark 時的引數,則可能丟擲 IOException。
如果未丟擲這樣的 IOException,則將該流重新設定為這種狀態:最近呼叫 mark 以來(或如果未呼叫 mark,則從檔案開始以來)讀取的所有位元組將重新提供給 read 方法的後續呼叫方,後接可能是呼叫 reset 時的下一輸入資料的所有位元組。
如果方法 markSupported 返回 false,則:
對 reset 的呼叫可能丟擲 IOException。
如果未丟擲 IOException,則將該流重新設定為一種固定狀態,該狀態取決於輸入流的特定型別和其建立方式的固定狀態。提供給 read 方法的後續呼叫方的位元組取決於特定型別的輸入流。
簡而言之就是:
呼叫mark方法會記下當前呼叫mark方法的時刻,InputStream被讀到的位置。
呼叫reset方法就會回到該位置。
舉個簡單的例子:
Java程式碼
看了這個例子之後對mark和reset介面有了很直觀的認識。
但是mark介面的引數readlimit究竟是幹嘛的呢?
我們知道InputStream是不支援mark的。要想支援mark子類必須重寫這三個方法,我想說的是不同的實現子類,mark的引數readlimit作用不盡相同。
常用的FileInputStream不支援mark。
1. 對於BufferedInputStream,readlimit表示:InputStream呼叫mark方法的時刻起,在讀取readlimit個位元組之前,標記的該位置是有效的。如果讀取的位元組數大於readlimit,可能標記的位置會失效。
在BufferedInputStream的read方法原始碼中有這麼一段:
Java程式碼
為什麼是可能會失效呢?
因為BufferedInputStream讀取不是一個位元組一個位元組讀取的,是一個位元組陣列一個位元組陣列讀取的。
例如,readlimit=35,第一次比較的時候buffer.length=0(沒開始讀)<readlimit
然後buffer陣列一次讀取48個位元組。這時的read方法只會簡單的挨個返回buffer陣列中的位元組,不會做這次比較。直到讀到buffer陣列最後一個位元組(第48個)後,才重新再次比較。這時如果我們讀到buffer中第47個位元組就reset。mark仍然是有效的。雖然47>35。
2. 對於InputStream的另外一個實現類:ByteArrayInputStream,我們發現readlimit引數根本就沒有用,呼叫mark方法的時候寫多少都無所謂。
Java程式碼
因為對於ByteArrayInputStream來說,都是通過位元組陣列建立的,內部本身就儲存了整個位元組陣列,mark只是標記一下陣列下標位置,根本不用擔心mark會建立太大的buffer位元組陣列快取。
3. 其他的InputStream子類沒有去總結。原理都是一樣的。
所以由於mark和reset方法配合可以記錄並回到我們標記的流的位置重新讀流,很大一部分就可以解決我們的某些重複讀的需要。
這種方式的優點很明顯:不用快取整個InputStream資料。對於ByteArrayInputStream甚至沒有任何的記憶體開銷。
當然這種方式也有缺點:就是需要通過干擾InputStream的讀取細節,也相對比較複雜。
其實InputStream本身提供了三個介面:
第一個,InputStream是否支援mark,預設不支援。
public boolean markSupported() {
return false;
}
第二個,mark介面。該介面在InputStream中預設實現不做任何事情。public synchronized void mark(int readlimit) {}
第三個,reset介面。該介面在InputStream中實現,呼叫就會拋異常。從三個介面定義中可以看出,首先InputStream預設是不支援mark的,子類需要支援mark必須重寫這三個方法。public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); }
第一個介面很簡單,就是標明該InputStream是否支援mark。
mark介面的官方文件解釋:
“在此輸入流中標記當前的位置。對 reset 方法的後續呼叫會在最後標記的位置重新定位此流,以便後續讀取重新讀取相同的位元組。
readlimit 引數告知此輸入流在標記位置失效之前允許讀取許多位元組。
mark 的常規協定是:如果方法 markSupported 返回 true,則輸入流總會在呼叫 mark 之後記住所有讀取的位元組,並且無論何時呼叫方法 reset ,都會準備再次提供那些相同的位元組。但是,如果在呼叫 reset 之前可以從流中讀取多於 readlimit 的位元組,則根本不需要該流記住任何資料。”
reset介面的官方文件解釋:
將此流重新定位到對此輸入流最後呼叫 mark 方法時的位置。
reset 的常規協定是:
如果方法 markSupported 返回 true,則:
如果建立流以來未呼叫方法 mark,或最後呼叫 mark 以來從該流讀取的位元組數大於最後呼叫 mark 時的引數,則可能丟擲 IOException。
如果未丟擲這樣的 IOException,則將該流重新設定為這種狀態:最近呼叫 mark 以來(或如果未呼叫 mark,則從檔案開始以來)讀取的所有位元組將重新提供給 read 方法的後續呼叫方,後接可能是呼叫 reset 時的下一輸入資料的所有位元組。
如果方法 markSupported 返回 false,則:
對 reset 的呼叫可能丟擲 IOException。
如果未丟擲 IOException,則將該流重新設定為一種固定狀態,該狀態取決於輸入流的特定型別和其建立方式的固定狀態。提供給 read 方法的後續呼叫方的位元組取決於特定型別的輸入流。
簡而言之就是:
呼叫mark方法會記下當前呼叫mark方法的時刻,InputStream被讀到的位置。
呼叫reset方法就會回到該位置。
舉個簡單的例子:
Java程式碼
- String content = "BoyceZhang!";
- InputStream inputStream = new ByteArrayInputStream(content.getBytes());
- // 判斷該輸入流是否支援mark操作
- if (!inputStream.markSupported()) {
- System.out.println("mark/reset not supported!");
- }
- int ch;
- boolean marked = false;
- while ((ch = inputStream.read()) != -1) {
- //讀取一個字元輸出一個字元
- System.out.print((char)ch);
- //讀到 'e'的時候標記一下
- if (((char)ch == 'e')& !marked) {
- inputStream.mark(content.length()); //先不要理會mark的引數
- marked = true;
- }
- //讀到'!'的時候重新回到標記位置開始讀
- if ((char)ch == '!' && marked) {
- inputStream.reset();
- marked = false;
- }
- }
- //程式最終輸出:BoyceZhang!Zhang!
看了這個例子之後對mark和reset介面有了很直觀的認識。
但是mark介面的引數readlimit究竟是幹嘛的呢?
我們知道InputStream是不支援mark的。要想支援mark子類必須重寫這三個方法,我想說的是不同的實現子類,mark的引數readlimit作用不盡相同。
常用的FileInputStream不支援mark。
1. 對於BufferedInputStream,readlimit表示:InputStream呼叫mark方法的時刻起,在讀取readlimit個位元組之前,標記的該位置是有效的。如果讀取的位元組數大於readlimit,可能標記的位置會失效。
在BufferedInputStream的read方法原始碼中有這麼一段:
Java程式碼
- } elseif (buffer.length >= marklimit) {
- markpos = -1; /* buffer got too big, invalidate mark */
- pos = 0; /* drop buffer contents */
- } else { /* grow buffer */
為什麼是可能會失效呢?
因為BufferedInputStream讀取不是一個位元組一個位元組讀取的,是一個位元組陣列一個位元組陣列讀取的。
例如,readlimit=35,第一次比較的時候buffer.length=0(沒開始讀)<readlimit
然後buffer陣列一次讀取48個位元組。這時的read方法只會簡單的挨個返回buffer陣列中的位元組,不會做這次比較。直到讀到buffer陣列最後一個位元組(第48個)後,才重新再次比較。這時如果我們讀到buffer中第47個位元組就reset。mark仍然是有效的。雖然47>35。
2. 對於InputStream的另外一個實現類:ByteArrayInputStream,我們發現readlimit引數根本就沒有用,呼叫mark方法的時候寫多少都無所謂。
Java程式碼
- publicvoid mark(int readAheadLimit) {
- mark = pos;
- }
- publicsynchronizedvoid reset() {
- pos = mark;
- }
因為對於ByteArrayInputStream來說,都是通過位元組陣列建立的,內部本身就儲存了整個位元組陣列,mark只是標記一下陣列下標位置,根本不用擔心mark會建立太大的buffer位元組陣列快取。
3. 其他的InputStream子類沒有去總結。原理都是一樣的。
所以由於mark和reset方法配合可以記錄並回到我們標記的流的位置重新讀流,很大一部分就可以解決我們的某些重複讀的需要。
這種方式的優點很明顯:不用快取整個InputStream資料。對於ByteArrayInputStream甚至沒有任何的記憶體開銷。
當然這種方式也有缺點:就是需要通過干擾InputStream的讀取細節,也相對比較複雜。