1. 程式人生 > >通過mark和reset方法重複利用InputStream

通過mark和reset方法重複利用InputStream

在上篇文章中我們已經簡單的知道可以通過快取InputStream來重複利用一個InputStream,但是這種方式的缺點也是明顯的,就是要快取一整個InputStream記憶體壓力可能是比較大的。如果第一次讀取InputStream是用來判斷檔案流型別,檔案編碼等用的,往往不需要所有的InputStream的資料,或許只需要前n個位元組,這樣一來,快取一整個InputStream實際上也是一種浪費。

其實InputStream本身提供了三個介面:
第一個,InputStream是否支援mark,預設不支援。
public boolean markSupported() {  
    return false;  
}  
第二個,mark介面。該介面在InputStream中預設實現不做任何事情。
public synchronized void mark(int readlimit) {} 
第三個,reset介面。該介面在InputStream中實現,呼叫就會拋異常。
public synchronized void reset() throws IOException {  
    throw new IOException("mark/reset not supported");  
}  
從三個介面定義中可以看出,首先InputStream預設是不支援mark的,子類需要支援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程式碼  收藏程式碼
  1. String content = "BoyceZhang!";  
  2. InputStream inputStream = new ByteArrayInputStream(content.getBytes());  
  3. // 判斷該輸入流是否支援mark操作
  4. if (!inputStream.markSupported()) {  
  5.     System.out.println("mark/reset not supported!");  
  6. }  
  7. int ch;    
  8. boolean marked = false;    
  9. while ((ch = inputStream.read()) != -1) {  
  10.     //讀取一個字元輸出一個字元  
  11.     System.out.print((char)ch);    
  12.     //讀到 'e'的時候標記一下
  13.      if (((char)ch == 'e')& !marked) {    
  14.         inputStream.mark(content.length());  //先不要理會mark的引數
  15.          marked = true;    
  16.      }    
  17.      //讀到'!'的時候重新回到標記位置開始讀
  18.       if ((char)ch == '!' && marked) {    
  19.           inputStream.reset();    
  20.           marked = false;  
  21.       }    
  22. }  
  23. //程式最終輸出:BoyceZhang!Zhang!

看了這個例子之後對mark和reset介面有了很直觀的認識。
但是mark介面的引數readlimit究竟是幹嘛的呢?
我們知道InputStream是不支援mark的。要想支援mark子類必須重寫這三個方法,我想說的是不同的實現子類,mark的引數readlimit作用不盡相同。
常用的FileInputStream不支援mark。
1. 對於BufferedInputStream,readlimit表示:InputStream呼叫mark方法的時刻起,在讀取readlimit個位元組之前,標記的該位置是有效的。如果讀取的位元組數大於readlimit,可能標記的位置會失效。

在BufferedInputStream的read方法原始碼中有這麼一段:
Java程式碼  收藏程式碼
  1. elseif (buffer.length >= marklimit) {  
  2.      markpos = -1;   /* buffer got too big, invalidate mark */
  3.      pos = 0;        /* drop buffer contents */
  4.      } 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程式碼  收藏程式碼
  1. publicvoid mark(int readAheadLimit) {  
  2.    mark = pos;  
  3. }  
  4. publicsynchronizedvoid reset() {  
  5.    pos = mark;  
  6. }  


因為對於ByteArrayInputStream來說,都是通過位元組陣列建立的,內部本身就儲存了整個位元組陣列,mark只是標記一下陣列下標位置,根本不用擔心mark會建立太大的buffer位元組陣列快取。

3. 其他的InputStream子類沒有去總結。原理都是一樣的。

所以由於mark和reset方法配合可以記錄並回到我們標記的流的位置重新讀流,很大一部分就可以解決我們的某些重複讀的需要。
這種方式的優點很明顯:不用快取整個InputStream資料。對於ByteArrayInputStream甚至沒有任何的記憶體開銷。
當然這種方式也有缺點:就是需要通過干擾InputStream的讀取細節,也相對比較複雜。