本文參考連接:
http://blog.csdn.net/class281/article/details/24849275
http://zhhphappy.iteye.com/blog/1562427
http://wdhdmx.iteye.com/blog/1279400
一、IO包簡要類圖
Java I/O流部分分為兩個模塊,即Java1.0中就有的面向字節的流(Stream),以及Java1.1中大幅改動添加的面向字符的流(Reader & Writer)。添加面向字符的流主要是為了支持國際化,舊的I/O流僅支持8位的字節流,並不能很好的處理16位的Unicode字符(Java的基礎類型char也是16位的Unicode)。下面就針對這兩類流做一個簡要的分析。
二、面向字節的流
InputStream(OutputStream)是所有面向字節流的基類。它的子類分為兩大塊,一是諸如ByteArrayInputStream(ByteArrayOutputStream),FileInputStream(FileOutputStream)等等面向各種不同的輸入(輸出)源的子類,另一塊為了方便流操作的而進一步封裝的裝飾器系列FilterInputStream(FilterOutputStream)類及其子類。
BufferedInputStream
BufferedInputStream 是一個帶有內存緩沖的 InputStream.
1.首先來看類結構 :
BufferedInputStream是繼承自FilterInputStream。
FilterInputStream繼承自InputStream屬於輸入流中的鏈接流,同時引用了InputStream,將InputStream封裝成一個內部變量,同時構造方法上需要傳入一個InputStream。這是一個典型的裝飾器模式,他的任何子類都可以對一個繼承自InputStream的原始流或其他鏈接流進行裝飾,如我們常用的使用BufferedInputStream對FileInputStream進行裝飾,使普通的文件輸入流具備了內存緩存的功能,通過內存緩沖減少磁盤io次數。
1 protected volatile InputStream in; 2 protected FilterInputStream(InputStream in) { 3 this.in = in; 4 }
注意:成員變量in使用了volatile關鍵字修飾,保障了該成員變量多線程情況下的可見性。
2.內存緩沖的實現
概要的了解完BufferedInputStream的繼承關系,接下來詳細理解BufferedInputStream是如何實現內存緩沖。既是內存緩沖,就涉及到內存的分配,管理以及如何實現緩沖。
通過構造方法可以看到:初始化了一個byte數組作為內存緩沖區,大小可以由構造方法中的參數指定,也可以是默認的大小。
1 protected volatile byte buf[]; 2 private static int defaultBufferSize = 8192; 3 public BufferedInputStream(InputStream in, int size) { 4 super(in); 5 if (size <= 0) { 6 throw new IllegalArgumentException("Buffer size <= 0"); 7 } 8 buf = new byte[size]; 9 } 10 public BufferedInputStream(InputStream in) { 11 this(in, defaultBufferSize); 12 }
看完構造函數,大概可以了解其實現原理:通過初始化分配一個byte數組,一次性從輸入字節流中讀取多個字節的數據放入byte數組,程序讀取部分字節的時候直接從byte數組中獲取,直到內存中的數據用完再重新從流中讀取新的字節。那么從api文檔中我們可以了解到BufferedStream大概具備如下的功能:
從api可以了解到BufferedInputStream除了使用一個byte數組做緩沖外還具備打標記,重置當前位置到標記的位置重新讀取數據,忽略掉n個數據。這些功能都涉及到緩沖內存的管理,首先看下相關的幾個成員變量:
1 protected int count; 2 protected int pos; 3 protected int markpos = -1; 4 protected int marklimit;
count表示當前緩沖區內總共有多少有效數據;pos表示當前讀取到的位置(即byte數組的當前下標,下次讀取從該位置讀取);markpos:打上標記的位置;marklimit:最多能mark的字節長度,也就是從mark位置到當前pos的最大長度。
從最簡單的read()讀取一個字節的方法開始看:
1 public synchronized int read() throws IOException { 2 if (pos >= count) { 3 fill(); 4 if (pos >= count) 5 return -1; 6 } 7 return getBufIfOpen()[pos++] & 0xff; 8 }
1 /** 2 * Fills the buffer with more data, taking into account 3 * shuffling and other tricks for dealing with marks. 4 * Assumes that it is being called by a synchronized method. 5 * This method also assumes that all data has already been read in, 6 * hence pos > count. 7 */ 8 private void fill() throws IOException { 9 byte[] buffer = getBufIfOpen(); 10 if (markpos < 0) 11 pos = 0; /* no mark: throw away the buffer */ 12 else if (pos >= buffer.length) /* no room left in buffer */ 13 if (markpos > 0) { /* can throw away early part of the buffer */ 14 int sz = pos - markpos; 15 System.arraycopy(buffer, markpos, buffer, 0, sz); 16 pos = sz; 17 markpos = 0; 18 } else if (buffer.length >= marklimit) { 19 markpos = -1; /* buffer got too big, invalidate mark */ 20 pos = 0; /* drop buffer contents */ 21 } else if (buffer.length >= MAX_BUFFER_SIZE) { 22 throw new OutOfMemoryError("Required array size too large"); 23 } else { /* grow buffer */ 24 int nsz = (pos <= MAX_BUFFER_SIZE - pos) ? 25 pos * 2 : MAX_BUFFER_SIZE; 26 if (nsz > marklimit) 27 nsz = marklimit; 28 byte nbuf[] = new byte[nsz]; 29 System.arraycopy(buffer, 0, nbuf, 0, pos); 30 if (!bufUpdater.compareAndSet(this, buffer, nbuf)) { 31 // Can't replace buf if there was an async close. 32 // Note: This would need to be changed if fill() 33 // is ever made accessible to multiple threads. 34 // But for now, the only way CAS can fail is via close. 35 // assert buf == null; 36 throw new IOException("Stream closed"); 37 } 38 buffer = nbuf; 39 } 40 count = pos; 41 int n = getInIfOpen().read(buffer, pos, buffer.length - pos); 42 if (n > 0) 43 count = n + pos; 44 }
/** * See the general contract of the <code>mark</code> * method of <code>InputStream</code>. * * @param readlimit the maximum limit of bytes that can be read before * the mark position becomes invalid. * @see java.io.BufferedInputStream#reset() */ public synchronized void mark(int readlimit) { marklimit = readlimit; markpos = pos; } /** * See the general contract of the <code>reset</code> * method of <code>InputStream</code>. * <p> * If <code>markpos</code> is <code>-1</code> * (no mark has been set or the mark has been * invalidated), an <code>IOException</code> * is thrown. Otherwise, <code>pos</code> is * set equal to <code>markpos</code>. * * @exception IOException if this stream has not been marked or, * if the mark has been invalidated, or the stream * has been closed by invoking its {@link #close()} * method, or an I/O error occurs. * @see java.io.BufferedInputStream#mark(int) */ public synchronized void reset() throws IOException { getBufIfOpen(); // Cause exception if closed if (markpos < 0) throw new IOException("Resetting to invalid mark"); pos = markpos; }
當pos>=count的時候也就是表示當前的byte中的數據為空或已經被讀完,他調用了一個fill()方法,從字面理解就是填充的意思,實際上是從真正的輸入流中讀取一些新數據放入緩沖內存中,之后直到緩沖內存中的數據讀完前都不會再從真正的流中讀取數據。
看源碼中的fill()方法有很大一段是關於markpos的處理,其處理過程大致如下圖:
a.沒有markpos的情況很簡單:
b.有mark的情況比較復雜:
3.read()方法返回值
以上即為內存緩沖管理的完全過程,再回過頭看read()方法,當緩沖byte數組中有數據可以讀時,直接從數組中讀取一個字節,但最后的read方法返回的卻是int,而且還和0xff做了與運算。
1 return getBufIfOpen()[pos++] & 0xff;
為什么不直接返回一個byte,而是一個與運算后的int。首先宏觀的看InputStream和Reader兩個輸入流的抽象類都定義了read接口而且都返回int,一個是字節流,一個是字符流。我們知道字節用byte表示,字符用char表示。首先看java中基本類型的取值范圍:
從取值范圍來看int包含了char和byte,這為使用int作為返回值類型提供了可能。
在應用中我們一般用read()接口的返回值是-1則表示已經讀到文件尾(EOF)。
char的取值范圍本身不包含負數,所有用int的-1表示文件讀完沒問題,但byte的取值范圍-128 ~ 127,包含了-1,讀取的有效數據范圍就是-128~127,沒辦法用這個取值范圍中的任何一個數字表示異常或者數據已經讀完,所以接口如果直接使用byte作為返回值不可行,直接將byte強制類型轉換成int也不行,因為如果讀到一個byte的-1,轉為int了也是-1,會被理解為文件已經讀完。所以這里做了一個特殊處理return getBufIfOpen()[pos++] & 0xff。
0xff是int類型,二進制為0000 0000 0000 0000 0000 0000 1111 1111。
上述的與運算實際上讀取的byte先被強制轉換成了int,例如byte的-1(最高位表示符號位,以補碼的形式表示負數為:1111 1111)
轉換為int之后的二進制1111 1111 1111 1111 1111 1111 1111 1111
& 0xff之后高位去0
最后返回的結果是0000 0000 0000 0000 0000 0000 1111 1111, 為int值為256
其-128~-1被轉為int中128~256的正數表示。
這樣解決了可以用-1表示文件已經讀完。但關鍵是數據的值發生了變化,真正要用讀取的數據時是否還能拿到原始的byte。還拿上面那個例子來看,當讀取返回一個256時,將其強制類型轉換為byte,(byte)256得到byte的-1,因為byte只有8位,當int的高位被丟棄后就只剩下1111 1111,在byte中高位的1表示符號位為負數,最終的結果即是byte的-1;同樣byte的-128(1000 0000)被轉為int的128(0000 0000 0000 0000 0000 0000 1000 0000),強制類型轉換后還原byte的1000 0000。
4.線程安全
返回值中還有一個細節是getBufIfOpen()[pos++],直接將pos++來獲取下一個未讀取的數據,這里涉及到的兩個元素:一個內存數組,一個當前讀取的數據下標都是全局變量,pos++也不是線程安全。那么BufferedInputStream如何保證對內存緩沖數組的操作線程安全?源碼中有操作的public方法除了close方法之外,其他方法上都加上了synchronized關鍵字,以保障上面描述的整個內存緩存數組的操作是線程安全的。但為什么close方法沒有synchronized,我們看這個方法做了些什么事情:
byte[] buffer; while ( (buffer = buf) != null) { if (bufUpdater.compareAndSet(this, buffer, null)) { InputStream input = in; in = null; if (input != null) input.close(); return; } // Else retry in case a new buf was CASed in fill() }
簡單來看做了兩個操作:把內存數組置為null,將引用的inputStream置為null,同時將引用的inputStream.close();
這兩個操作的核心都是關閉原始流,釋放資源,如果加了synchronized關鍵字,會導致當前線程正在執行read方法,而且系統消耗很大時,想釋放資源無法釋放。此時read方法還沒執行完,我們知道synchronized的鎖是加在整個對象上的,所以close方法就必須等到read結束后才能執行,這樣很明顯不能滿足close的需求,甚至會導致大量的io資源被阻塞不能關閉。
但該方法用一個while循環,而且只有當bufUpdater.compareAndSet(this, buffer, null)成功時,才執行上述的資源釋放。
先看bufUpdater這個全局變量
protected volatile byte buf[]; private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater = AtomicReferenceFieldUpdater.newUpdater (BufferedInputStream.class, byte[].class, "buf");
AtomicReferenceFieldUpdater是一個抽象類,但該類的內部已經給出了包訪問控制級別的一個實現AtomicReferenceFieldUpdaterImpl,原理是利用反射將一個 被聲明成volatile 的屬性通過JNI調用,使用cpu指令級的命令將一個變量進行更新,保障該操作是原子的。也就是通過上面定義的bufUpdater將buf這個byte數組的跟新變為原子操作,其作用是保障其原子更新。
BufferedInputStream源代碼中總共有兩個地方用到了這個bufUpdater,一個是我們上面看到的close方法中,另外一個是再前面說道的fill()方法中。既然BufferedInputStream的所有操作上都用了synchronized來做同步,那為什么這里還需要用這個原子更新器呢?帶着問題上面提到過fill()方法中的最后一個步驟:當有mark,而且markLimit的長度又大於初始數組的長度時,需要對內存數組擴容,即創建一個尺寸更大的數組,將原來數組中的數據拷貝到新數組中,再將指向原數組的應用指向新的數組。bufUpdater正是用在了將原數組引用指向新數組的操作上,同樣close的方法使用的bufUpdater也是用在對數組引用的改變上,這樣看來就比較清晰了,主要是為了防止一個線程在執行close方法時,將buffer賦值為null這個時候另外一個線程正在執行fill()方法的最后一個步驟又將buffer賦值給了一個新的數組,從而導致資源沒有釋放掉。
BufferedOutputStream
下面看一下 bufferedOutputStream的源碼,
相比之下緩沖輸出流就簡單很多,基本上write方法可以說明一切了:
-
在緩沖區(也是字節數組)寫滿之前調用write方法只是將數據復制到內部緩沖區;
-
在緩沖區寫滿之后,現將舊的數據寫入到底層輸出流,然后將新的數據暫存到緩沖區(新的數據並未同時寫入)
於是flush方法也找到了存在的意義:將現有數據全部寫入。
1 package java.io; 2 3 public class BufferedOutputStream extends FilterOutputStream { 4 // 保存“緩沖輸出流”數據的字節數組 5 protected byte buf[]; 6 7 // 緩沖中數據的大小 8 protected int count; 9 10 // 構造函數:新建字節數組大小為8192的“緩沖輸出流” 11 public BufferedOutputStream(OutputStream out) { 12 this(out, 8192); 13 } 14 15 // 構造函數:新建字節數組大小為size的“緩沖輸出流” 16 public BufferedOutputStream(OutputStream out, int size) { 17 super(out); 18 if (size <= 0) { 19 throw new IllegalArgumentException("Buffer size <= 0"); 20 } 21 buf = new byte[size]; 22 } 23 24 // 將緩沖數據都寫入到輸出流中 25 private void flushBuffer() throws IOException { 26 if (count > 0) { 27 out.write(buf, 0, count); 28 count = 0; 29 } 30 } 31 32 // 將“數據b(轉換成字節類型)”寫入到輸出流中 33 public synchronized void write(int b) throws IOException { 34 // 若緩沖已滿,則先將緩沖數據寫入到輸出流中。 35 if (count >= buf.length) { 36 flushBuffer(); 37 } 38 // 將“數據b”寫入到緩沖中 39 buf[count++] = (byte)b; 40 } 41 42 public synchronized void write(byte b[], int off, int len) throws IOException { 43 // 若“寫入長度”大於“緩沖區大小”,則先將緩沖中的數據寫入到輸出流,然后直接將數組b寫入到輸出流中 44 if (len >= buf.length) { 45 flushBuffer(); 46 out.write(b, off, len); 47 return; 48 } 49 // 若“剩余的緩沖空間 不足以 存儲即將寫入的數據”,則先將緩沖中的數據寫入到輸出流中 50 if (len > buf.length - count) { 51 flushBuffer(); 52 } 53 System.arraycopy(b, off, buf, count, len); 54 count += len; 55 } 56 57 // 將“緩沖數據”寫入到輸出流中 58 public synchronized void flush() throws IOException { 59 flushBuffer(); 60 out.flush(); 61 } 62 }
FileInputStream & FileOutputStream
以文件為輸入輸出目標的類,其實也可以想象得到,讀寫本地文件的類追溯上去肯定是本地方法。所以當然它的一系列read(write)方法都是native的。這個以后如果以機會的話再研究。
目前能看到的輔助功能有:
- FileInputStream(FileOutputStream)利用ThreadLocal類來判斷打開這個流的線程數(ThreadLocal中定義了一個two-sized ThreadLocalMap,具體原理待我看完HashMap再回來 /_\)。
- 都可以調用getChannel()方法,利用sun.nio.ch.FileChannelImpl類返回一個FileChannel對象,即可以將文件流轉為通道操作。
讀寫文件的這一溜方法大都在sun.nio.ch和sun.misc包里,有興趣的可以去看openjdk提供的源碼,不過Java里面也是調用native方法,而且考慮到跨平台特性估計設計上也會更加復雜,所以推薦先去了解C的文件讀寫。
ByteArrayInputStream & ByteArrayOutputStream
這兩個類在很多地方被翻譯成內存輸入(輸出)流,當時俺就被這高大上的名字深深的折服了。
其實它們的功能、實現都非常簡單,先把所有的數據全存到它內部的字節數組里,然后用這個數組來繼續讀寫,這個時候底層流你就可以不用管了,愛關就關沒有影響。
插播:ByteArrayInputStream vs BufferedInputStream
經常會有人把這兩個類混在一起,於是特地在此比划一番。說到它們的區別,但實際上從類的組織結構上可以看出來,這兩個類其實沒有什么聯系:一個是以內存中的字節數組為輸入目標的類,一個是為了更好的操作字節輸入流而提供的帶有緩沖區的裝飾器類。它們的使用目的本就不一樣,只不過由於名字似曾相識,打扮得(實現方式)也差不多,所以經常被誤認為兩兄弟。這兩兄弟的差距還是蠻大的:
-
ByteArrayInputStream需要在內部的保存流的所有數據,所以需要一個足夠大的字節數組,但數組的容量是受JVM中堆空間大小限制的,更極端的情況,即使你為JVM分配了一個很大的空間,由於Java數組使用的是int型索引,所以你也猜到了,它還是會被限制在INT_MAX范圍以內。在底層流數據全部保存到ByteArrayInputStream后,你就可以不用再管流,轉而去從ByteArrayInputStream讀取數據了。
-
而BufferedInputStream只會用一個有限大小的緩存數組保存底層流的一小部分數據,你在讀取數據的時候其實還是在和底層流打交道,只不過BufferedInputStream為了滿足你變幻莫測的讀取要求提供了緩沖區,讓你的讀取操作更加犀利流暢。
所以總結起來,它們除了同樣從InputStream派生而來,同樣使用了字節數組(這是個經常發生的巧合)以外,沒有任何聯系。
DataInputStream & DataOutputStream
這兩個類允許我們以基本類型的形式操作字節流,可以理解為一個基本數據類型到字節之間的映射轉換。
例如我想要從輸入流中讀取一個int類型數據(4字節),那么就需要先讀4個字節,然后按照每個字節在int中的位置作相應移位處理,就得到這4個字節所代表的int型數據了。
PushbackInputStream
這個類的功能其實比較隱晦(至少我一開始是理解錯了)。按照字面意思理解,它應該是為了在讀取一定量字節之后,允許我們調用unread方法,重讀這部分字節。實現上,它的內部也有一個字節數組作緩沖,恩,看起來一切正常。
可是測試它的unread(byte[])方法的時候發現被坑了,調用unread(byte[])之后再讀取,讀到的其實是你push進去的這個字節數組。它其實沒有想象的那么聰明,在調用unread(byte[])方法的時候,只是很萌的把你傳給它的這個字節數組當成你之前讀取的數據,把它直接復制到內部緩沖區里。
也就是說,完全是 push what, get what…
其他
ObjectInputStream(ObjectOutputStream)與Serializable接口等一起構成了Java的序列化機制,其中牽涉到對象數據的描述、保存與恢復,在此暫不討論。
面向字符的流(Reader & Writer)
Reader(Writer)是所有面向字符的流的基類。它同樣有為了適配各種不同輸入源的子類如PipedReader(PipedWriter)、CharArrayReader(CharArrayWriter)等,其中的FileReader(FileWriter)類是直接繼承自InputStreamReader(OutputStreamWriter)。故在此主要關注以Stream為輸入輸出的InputStreamReader(OutputStreamWriter)類,以及BufferedReader(BufferedWriter)等裝飾器類。
BufferedReader & BufferedWriter
BufferedInputStream | BufferedReader |
---|---|
count | nChars |
pos | nextChar |
markpos | markedChar |
marklimit | readAheadLimit |
下面貼一下BufferedReader和BufferedWriter一些主要方法的源碼
1.BufferedReader
1.1 繼承關系
public class BufferedReader extends Reader { //這個又是裝飾模式 private Reader in; }
1.2 構造方法
1 public BufferedReader(Reader in) { 2 this(in, defaultCharBufferSize); 3 } 4 //默認緩存數組的大小 5 private static int defaultCharBufferSize = 8192; 6 //構造方法 7 public BufferedReader(Reader in, int sz) { 8 //這個方法可參考前面的Writer源碼,只要是將鎖賦值 9 super(in); 10 if (sz <= 0) 11 throw new IllegalArgumentException("Buffer size <= 0"); 12 //裝飾模式。。 13 this.in = in; 14 cb = new char[sz]; 15 nextChar = nChars = 0; 16 } 17 //兩個上面用到的參數,用於緩存數據,是字符(char)數組,不是字節(byte)數組。 18 private char cb[]; 19 private int nChars, nextChar;
1.3 標記有關
在看read方法之前先看一眼 標記mark有關的方法有點幫助。為看懂read做鋪墊
1 //標記流中的當前位置,帶入的參數表示標記所占的空間 2 public void mark(int readAheadLimit) throws IOException { 3 if (readAheadLimit < 0) { 4 throw new IllegalArgumentException("Read-ahead limit < 0"); 5 } 6 synchronized (lock) { 7 ensureOpen(); 8 this.readAheadLimit = readAheadLimit; 9 markedChar = nextChar; 10 markedSkipLF = skipLF; 11 } 12 } 13 //回到標記位置 14 public void reset() throws IOException { 15 synchronized (lock) { 16 ensureOpen(); 17 if (markedChar < 0) 18 throw new IOException((markedChar == INVALIDATED) 19 ? "Mark invalid" 20 : "Stream not marked"); 21 //下面兩個參數在讀方法中會有詳細解釋 22 nextChar = markedChar; 23 skipLF = markedSkipLF; 24 } 25 }
1.4 read
這個方法中fill()是重點,有點繞,但看懂后就覺得很清晰,能完全理解bufferedReader的原理。
看完這個方法再回去看3.3的標記部分,就很容易看懂。
1 public int read() throws IOException { 2 //鎖,看來讀得時候也只能一個方法讀。 3 synchronized (lock) { 4 //確保輸入流不是空。 5 ensureOpen(); 6 //這個循環和while一樣。 7 for (;;) { 8 //下面的判斷為是否下一個讀取的字符超出了緩存數組中實際包含數據的大小。 9 if (nextChar >= nChars) { 10 //下一個字符超出或者等於緩存數組的大小 11 //這個是核心的方法,里面有標記的內容,詳細的看下面內容。 12 fill(); 13 //如果還是超出,則表示輸入流讀完了。 14 if (nextChar >= nChars) 15 return -1; 16 } 17 //如果下一個字符是換行符.這個變量只有在readLine里面才變為true。和\n\r有關,可忽略。針對不同的平台的 18 if (skipLF) { 19 skipLF = false; 20 if (cb[nextChar] == '\n') { 21 nextChar++; 22 continue; 23 } 24 } 25 //返回當前讀的字符,並將要讀字符+1 26 return cb[nextChar++]; 27 } 28 } 29 } 30 31 //下面的變量是用於fill方法里的 32 //下面兩個變量是標記的狀態, -1為未啟動標記,-2為標記失效。 33 private static final int INVALIDATED = -2; 34 private static final int UNMARKED = -1; 35 //標記的位置 36 private int markedChar = UNMARKED; 37 //nChars表示現在緩存數組中已經存在多少個字符。 38 //nextChar表示下一個讀取的位置,從0開始,這個只是緩存數組中的位置,並不是讀取流的位置。 39 private int nChars, nextChar; 40 //標記分配的空間大小。超出后,如果緩存數組重新處置,則標記失效。 41 private int readAheadLimit = 0; 42 43 //將字符數組讀滿,然后直接返回數組中的某個值。里面主要考慮的是標記的問題。 44 //這個和BufferedInputStream差不多,一個是byte[],這個是char[] 45 private void fill() throws IOException { 46 //計算這次緩存數據的起始位置,起始位置之前保存的是標記的內容。 47 int dst; 48 if (markedChar <= UNMARKED) { 49 //這里表示沒有使用標記,或者標記失效。 50 dst = 0; 51 } else { 52 //表示使用標記 53 //這個變量表示標記之后實際使用了多少空間 54 int delta = nextChar - markedChar; 55 if (delta >= readAheadLimit) { 56 //如果超過了標記初始的空間。 57 //標記失效 58 markedChar = INVALIDATED; 59 //標記空間賦0 60 readAheadLimit = 0; 61 //緩存數據起點0 62 dst = 0; 63 } else { 64 //如果未超過標記初始的空間。 65 if (readAheadLimit <= cb.length) { 66 //分配的標記空間小於緩存數組的長度 67 //將標記后實際使用長度復制到數組的開始。 68 System.arraycopy(cb, markedChar, cb, 0, delta); 69 //將標記的位置賦0,標記所占空間仍然是原來的空間,不會縮小。 70 markedChar = 0; 71 //數據緩存的起點 72 dst = delta; 73 } else { 74 //長度不夠,新建一個。 75 char ncb[] = new char[readAheadLimit]; 76 //和上面一樣,復制標記到最前面 77 System.arraycopy(cb, markedChar, ncb, 0, delta); 78 //將引用更新 79 cb = ncb; 80 markedChar = 0; 81 dst = delta; 82 } 83 nextChar = nChars = delta; 84 } 85 } 86 //下面是讀數據,讀出一定長度,默認cb的長度8192,cb在BufferedReader中是唯一的緩存數組。 87 int n; 88 //這個地方讀的方法中不可能返回0.所以只會執行一次 89 do { 90 //從標簽之后讀,讀滿cb字符數組,注意,這里是調用in的讀方法。 91 n = in.read(cb, dst, cb.length - dst); 92 } while (n == 0); 93 //讀到數據的情況,沒有讀到的話就不做任何操作。 94 if (n > 0) { 95 //現在緩存空間中已有的真實緩存數量 96 nChars = dst + n; 97 //下一個讀取的位置。 98 nextChar = dst; 99 } 100 }
其它的read方法和這個類似。
一次讀出很多字符的時候,處理的策略是:
a.緩存數組不夠,就用in直接讀,不經過緩存.
b.緩存數組夠,就將緩存中讀出。
c.緩存數組夠,但讀完后還沒讀滿,則繼續從in中接着讀,不夠的部分不過緩存數組。
1.5 readLine
這個是用的比較多的方法,所以列出來。這個方法在有上面的基礎上,還是很好懂的。
1 String readLine(boolean ignoreLF) throws IOException { 2 //傳入的布爾值默認為false 3 StringBuffer s = null; 4 int startChar; 5 6 synchronized (lock) { 7 ensureOpen(); 8 boolean omitLF = ignoreLF || skipLF; 9 //這個是什么?goto? 10 bufferLoop: 11 //while 12 for (;;) { 13 //下一個字符超出緩存數組大小,這里nextChar是從0開始的,所以相等的時候就代表已經超出了緩存數組范圍。 14 if (nextChar >= nChars) 15 fill(); 16 //下面的if是判斷流的末尾,讀完了就返回null,或者將之前讀的內容返回 17 if (nextChar >= nChars) { 18 if (s != null && s.length() > 0) 19 return s.toString(); 20 else 21 return null; 22 } 23 //表示沒有到末尾. 24 boolean eol = false; 25 char c = 0; 26 int i; 27 //這個是處理\r\n的情況,不進行兩次判斷,忽略 28 if (omitLF && (cb[nextChar] == '\n')) 29 nextChar++; 30 skipLF = false; 31 omitLF = false; 32 33 charLoop: 34 //遍歷緩存數組,直到\n或者\r 35 for (i = nextChar; i < nChars; i++) { 36 c = cb[i]; 37 if ((c == '\n') || (c == '\r')) { 38 //表示讀取到了換行 39 eol = true; 40 break charLoop; 41 } 42 } 43 //記錄讀取開始的地方, 44 startChar = nextChar; 45 //要讀的下一個字符。 46 nextChar = i; 47 //讀取到換行,而不是讀完緩存。 48 if (eol) { 49 String str; 50 if (s == null) { 51 str = new String(cb, startChar, i - startChar); 52 } else { 53 s.append(cb, startChar, i - startChar); 54 str = s.toString(); 55 } 56 nextChar++; 57 if (c == '\r') { 58 skipLF = true; 59 } 60 return str; 61 } 62 //表示讀完了緩存數組,還需要繼續讀。 63 if (s == null) 64 s = new StringBuffer(defaultExpectedLineLength); 65 s.append(cb, startChar, i - startChar); 66 } 67 } 68 }
1.6 其它方法
skip 就是先把緩存數組中跳過去,如果緩存數組不夠,就再將數據讀入緩存數組,再跳,一直循環。
ready 表示緩存是否讀完了,沒什么用處。
markSupported 是否支持標記
close 流等需要關閉的東西都關閉。
2.BufferedWriter
2.1 繼承關系
1 public class BufferedWriter extends Writer { 2 //裝飾模式 3 private Writer out; 4 }
2.2 構造函數
將緩存數組初始化,並且根據平台初始化換行符號。
1 public BufferedWriter(Writer out, int sz) { 2 super(out); 3 if (sz <= 0) 4 throw new IllegalArgumentException("Buffer size <= 0"); 5 this.out = out; 6 cb = new char[sz]; 7 nChars = sz; 8 nextChar = 0; 9 //獲取換行的符號\n \r,和方法System.getProperty("line.separator")一樣 10 lineSeparator = (String) java.security.AccessController.doPrivileged( 11 new sun.security.action.GetPropertyAction("line.separator")); 12 }
2.3 write有關
1 public void write(int c) throws IOException { 2 synchronized (lock) { 3 //判斷是否有輸出流 4 ensureOpen(); 5 //如果緩存數組寫滿了,就flush數組。 6 if (nextChar >= nChars) 7 flushBuffer(); 8 //將內容寫入緩存數組中 9 cb[nextChar++] = (char) c; 10 } 11 } 12 //flush,這個方法就知道flush的重要性, 13 void flushBuffer() throws IOException { 14 synchronized (lock) { 15 ensureOpen(); 16 if (nextChar == 0) 17 return; 18 //將數據寫入流 19 out.write(cb, 0, nextChar); 20 nextChar = 0; 21 } 22 }
用的比較多的是寫字符串。就是將字符串轉變成字符數組。
1 public void write(String s, int off, int len) throws IOException { 2 synchronized (lock) { 3 ensureOpen(); 4 5 int b = off, t = off + len; 6 while (b < t) { 7 int d = min(nChars - nextChar, t - b); 8 //將字符串轉為字符數組 9 s.getChars(b, b + d, cb, nextChar); 10 //寫入緩存數組 11 b += d; 12 nextChar += d; 13 if (nextChar >= nChars) 14 //如果寫滿了,就寫入流中。 15 flushBuffer(); 16 } 17 } 18 }
2.4 其它
a.writeLine 寫一個換行
1 public void newLine() throws IOException { 2 //同樣寫到緩存數組里 3 write(lineSeparator); 4 }
b.flush,這個也不多說了。
1 public void flush() throws IOException { 2 synchronized (lock) { 3 flushBuffer(); 4 out.flush(); 5 } 6 }
c.close 關閉所有該關閉的.
1 public void close() throws IOException { 2 synchronized (lock) { 3 if (out == null) { 4 return; 5 } 6 try { 7 //最后還釋放了一次。不過沒有執行flush方法,所以在close前還是要執行一次flush! 8 flushBuffer(); 9 } finally { 10 out.close(); 11 out = null; 12 cb = null; 13 } 14 } 15 }
InputStreamReader & OutputStreamWriter
這是兩個適配器類,目的就是為了轉換字節與字符。
實現上,使用了sun.nio.cs包中的StreamDecoder(StreamEncoder),而這兩個類都是使用java.nio.charset中的Charset(提供Unicode字符與相應字節的對應關系),以及CharsetDecoder,CharsetEncoder,ok到此打住,編碼解碼問題以后再說。。。