Java FileReader InputStreamReader类源码解析
FileReader
前面介紹FileInputStream的時候提到過,它是從文件讀取字節,如果要從文件讀取字符的話可以使用FileReader。FileReader是可以便利讀取字符文件的類,構造器只能使用默認的字符集編碼(系統的默認字符集)、默認的bytebuffer大小8KB。如果想要自己指定這些值的話,可以直接通過FileInputStream構造一個InputStreamReader而不使用FileInputStream。
FileReader本身的代碼其實沒有什么可以分析的,就只有下面幾行,它的操作全部是基于父類來進行的。傳入文件路徑名、具體的文件或者文件描述符來構造一個文件字節輸入流,字符輸入流是基于字節流上轉換的。
public class FileReader extends InputStreamReader {public FileReader(String fileName) throws FileNotFoundException {super(new FileInputStream(fileName));}public FileReader(File file) throws FileNotFoundException {super(new FileInputStream(file));}public FileReader(FileDescriptor fd) {super(new FileInputStream(fd));}}InputStreamReader
然后我們來看下FileReader的父類InputStreamReader,它是從字節流到字符流的橋梁:它讀取字節并使用特定的字符集解碼成字符。字符集可能通過名字來確定或者直接特別給出或者是平臺的默認字符集。
InputStreamReader的每一個read方法的調用可能會引起一個或多個字節從字節輸出流中被讀取。為了使字節能夠有效的轉換為字符,可能會提前從流中讀取比當前讀取操作所需字節數更多的字節。
為了提高效率,考慮將InputStreamReader嵌入到BufferedReader中,比如BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
InputStreamReader繼承了抽象類Reader,Reader中實現了一些具體方法,這些方法沒有在InputStreamReader中重寫,比如skip方法。InputStreamReader有一個核心內部變量StreamDecoder,這個類的作用是將輸入的字節轉換為字符,后面會具體分析。
InputStreamReader的構造函數有4種重載,必須的參數是InputStream,可選的是字符集參數可以輸入字符集的名字、直接指定字符集或者構造一個CharsetDecoder作為參數
//創建一個使用默認字符集的InputStreamReaderpublic InputStreamReader(InputStream in) {super(in);//Reader的lock是InputStreamtry {sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object} catch (UnsupportedEncodingException e) {// The default encoding should always be available默認的字符集總是有效的,所以無參構造不會拋出throw new Error(e);}}//創建一個使用指定名字字符集的InputStreamReaderpublic InputStreamReader(InputStream in, String charsetName)throws UnsupportedEncodingException{super(in);if (charsetName == null)throw new NullPointerException("charsetName");sd = StreamDecoder.forInputStreamReader(in, this, charsetName);}//創建一個使用給出的字符集的InputStreamReaderpublic InputStreamReader(InputStream in, Charset cs) {super(in);if (cs == null)throw new NullPointerException("charset");sd = StreamDecoder.forInputStreamReader(in, this, cs);}//創建一個使用給出的字符集解碼器的InputStreamReaderpublic InputStreamReader(InputStream in, CharsetDecoder dec) {super(in);if (dec == null)throw new NullPointerException("charset decoder");sd = StreamDecoder.forInputStreamReader(in, this, dec);}getEncoding方法通過StreamDecoder提供的getEncoding()返回這個流使用的字符編碼名字,如果編碼有歷史名則返回它,如果沒有的話返回官方名字。如果這個對象是通過InputStreamReader(InputStream, String)構造的,返回的名字可能給傳給構造函數的不同,如果流已經被關閉會返回null。
public String getEncoding() {return sd.getEncoding();}read、ready和close方法都是直接調用StreamDecoder對應的方法
//讀取單個字符public int read() throws IOException {return sd.read();}//讀取字符到一個數組中,返回讀取的字符數,如果開始前就已經到達末尾則返回-1public int read(char cbuf[], int offset, int length) throws IOException {return sd.read(cbuf, offset, length);}//該流是否準備完畢讀取。當輸入緩沖區是非空時,或者字節能夠從下方的字節流讀取時,InputStreamReader是準備完的public boolean ready() throws IOException {return sd.ready();}//關閉輸入流,釋放資源public void close() throws IOException {sd.close();}Reader
再來看InputStreamReader的父類Reader,它是一個字符讀取流的抽象類,子類必須實現的方法只有read(char[], int, int)和close()。但是大部分子類會重寫這里定義的一些方法來獲取更高的效率或者更多的功能。實現了兩個接口:Readable是字符來源,實現了這個接口字符可以通過CharBuffer來讀取,Closeable接口代表實現類對象可以關閉來釋放資源,比如打開的文件。根據對這個類和InputStreamReader的分析,InputStreamReader直接使用了Reader的skip方法也就是讀取到內存后丟棄,所以比起能夠直接位移的實現方法效率不佳,此外也不能往回跳和使用mark/reset。
存在一個內部變量lock,這個對象用于流中的同步操作。為了提高效率,一個字符流對象可能使用一個對象而不是它自己來保護臨界區。因此子類應該使用這個對象而不是this或者synchronized方法。InputStreamReader傳入作為lock對象的是自己對象本身。
構造函數可以傳入指定的lock對象,不傳入的話使用對象自身
protected Object lock;//創建一個新的字符流reader,它的臨界區依靠它自己來同步protected Reader() {this.lock = this;}//創建一個新的字符流reader,它的臨界區依靠提供的對象來同步protected Reader(Object lock) {if (lock == null) {throw new NullPointerException();}this.lock = lock;}read方法根據重載的輸入參數不同,可以讀取單個字符也一個讀取多個字符到數組或者抽象類CharBuffer中,但是最終這些方法都是基于抽象方法read(char cbuf[], int off, int len),也是子類必須要實現的方法之一
//嘗試讀取字符到具體的字符緩沖區中,緩沖區作為字符的倉庫:唯一的變更是put操作的結果,沒有翻轉或者倒回的操作。public int read(java.nio.CharBuffer target) throws IOException {int len = target.remaining();//讀取長度是緩沖區剩余的空間,也就是盡量填滿緩沖區char[] cbuf = new char[len];int n = read(cbuf, 0, len);//將字符讀取到char數組中,實現隨子類決定,n=讀取到的字符數if (n > 0)target.put(cbuf, 0, n);//將數組中的字符復制到CharBuffer,根據CharBuffer子類的實現方法具體操作不同return n;}//讀取一個單一的字符,這個方法會阻塞,直到有一個有效字符,或者一個IO錯誤發生或者,或者到達了流的尾端public int read() throws IOException {char cb[] = new char[1];if (read(cb, 0, 1) == -1)return -1;elsereturn cb[0];}//將字符讀取到數組中public int read(char cbuf[]) throws IOException {return read(cbuf, 0, cbuf.length);}abstract public int read(char cbuf[], int off, int len) throws IOException;Reader默認實現了skip方法,跳過n個字符,最大一次跳8KB,不能回跳,InputStreamReader沒有重寫這個方法。通過將字符讀取到skipBuffer數組然后丟棄來實現,所以會增加gc的工作量,效率不佳。
/** 最大跳過緩沖區大小8K */private static final int maxSkipBufferSize = 8192;/** 跳過緩沖區在分配前為null */private char skipBuffer[] = null;public long skip(long n) throws IOException {if (n < 0L)//跳過負數會拋出異常,也就是不能回跳,這點和FileInputStream不同throw new IllegalArgumentException("skip value is negative");int nn = (int) Math.min(n, maxSkipBufferSize);//最多跳過8KBsynchronized (lock) {//skip操作是同步的if ((skipBuffer == null) || (skipBuffer.length < nn))skipBuffer = new char[nn];long r = n;while (r > 0) {int nc = read(skipBuffer, 0, (int)Math.min(r, nn));//通過讀取后丟棄來實現跳躍if (nc == -1)break;r -= nc;}return n - r;}}ready告知這個流是否準備完讀取數據,若不重寫則永遠返回false
public boolean ready() throws IOException {return false;}Reader默認也不支持mark/reset,所以以下幾個方法不重寫無法使用
//告知這個流是否支持mark()操作public boolean markSupported() {return false;}//標記當前位置,然后可以通過reset()回跳public void mark(int readAheadLimit) throws IOException {throw new IOException("mark() not supported");}public void reset() throws IOException {throw new IOException("reset() not supported");}close關閉流并釋放相關的系統資源,一旦流被關閉,其他操作會拋出異常IOException。關閉一個已經關閉的流沒有作用。是必須實現的兩個類之一。
abstract public void close() throws IOException;StreamDecoder
最后要來分析的是sun.nio.cs.StreamDecoder,這個包里的源碼在OracleJDK里是沒有的,所以我去找了OpenJDK8里對應的源碼來分析。這個類的作用是將字節解析為字符的解碼器,繼承了抽象類Reader。因為eclipse無法在sun包里的代碼中加斷點,所以只能肉眼調試了,可能以下分析的具體操作會有些出入,但總體思路應該問題不大。
StreamDecoder一次至少要讀取兩個字符,如果調用者只需要一個字符,那么會將一個字符緩存在leftoverChar,下次需要讀取時再加入到返回內容中。所以,使用了這個StreamDecoder的對象只能采取讀取后丟棄的方式進行skip,否則會出現內容錯亂。
// 為了解決替換問題我們決不能嘗試一次產生少于兩個字符。如果我們只要求返回一個字符,另外一個會存在這里之后再返回private boolean haveLeftoverChar = false;private char leftoverChar;StreamDecoder只有在流關閉前才能工作,關閉后的操作會拋出IOException
private volatile boolean isOpen = true;// 流是否打開private void ensureOpen() throws IOException {if (!isOpen)throw new IOException("Stream closed");}StreamDecoder自己的構造函數是一個包所有權的方法,所以包外的類不能夠直接使用構造函數
private Charset cs;private CharsetDecoder decoder;private ByteBuffer bb;// 下面兩個有一個不是nullprivate InputStream in;private ReadableByteChannel ch;StreamDecoder(InputStream in, Object lock, Charset cs) {this(in, lock, cs.newDecoder().onMalformedInput(CodingErrorAction.REPLACE)// 有畸形輸入錯誤時解碼器丟棄錯誤的輸入,替換為替代值然后繼續后面的操作.onUnmappableCharacter(CodingErrorAction.REPLACE));// 有不可用圖形表示的字符錯誤出現時解碼器丟棄錯誤的輸入,替換為替代值然后繼續后面的操作}//實際執行的構造函數StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {super(lock);// lock是InputStreamReader對象本身this.cs = dec.charset();this.decoder = dec;// 在directbuffer更快前不會進入這個代碼塊,實際上因為堆外內存的操作速度不如堆內內存所以這段是被棄用的if (false && in instanceof FileInputStream) {ch = getChannel((FileInputStream) in);if (ch != null)bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE);}if (ch == null) {this.in = in;this.ch = null;bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);//分配一個大小為8K的堆內ByteBuffer}bb.flip(); // 為初始狀態為空/*flip的作用有兩個:1. 把limit設置為當前的position值2. 把position設置為0然后處理的數據就是從position到limit直接的數據,也就是你剛剛讀取過來的數據*/}//從ReadableByteChannel讀取數據StreamDecoder(ReadableByteChannel ch, CharsetDecoder dec, int mbc) {this.in = null;this.ch = ch;this.decoder = dec;this.cs = dec.charset();this.bb = ByteBuffer.allocate(mbc < 0 ? DEFAULT_BYTE_BUFFER_SIZE : (mbc < MIN_BYTE_BUFFER_SIZE ? MIN_BYTE_BUFFER_SIZE : mbc));//mbc是ByteBuffer初始大小,為負數時取8KB,小于32B時取32Bbb.flip();}我們可以看到在InputStreamReader中,構造是通過工廠模式StreamDecoder.forInputStreamReader(in, this, charsetName)來完成的,這里會調用構造函數來構造StreamDecoder對象。這里forInputStreamReader是用于InputStreamReader的,而forDecoder則是用于java.nio.channels.Channels.newReader
// java.io.InputStreamReader工廠模式public static StreamDecoder forInputStreamReader(InputStream in, Object lock, String charsetName)throws UnsupportedEncodingException {String csn = charsetName;if (csn == null)csn = Charset.defaultCharset().name();// 若沒有給出字符集名字則使用平臺默認字符集try {if (Charset.isSupported(csn))return new StreamDecoder(in, lock, Charset.forName(csn));// lock是InputStreamReader對象本身} catch (IllegalCharsetNameException x) {}throw new UnsupportedEncodingException(csn);}public static StreamDecoder forInputStreamReader(InputStream in, Object lock, Charset cs) {return new StreamDecoder(in, lock, cs);}public static StreamDecoder forInputStreamReader(InputStream in, Object lock, CharsetDecoder dec) {return new StreamDecoder(in, lock, dec);}// java.nio.channels.Channels.newReader工廠模式public static StreamDecoder forDecoder(ReadableByteChannel ch, CharsetDecoder dec, int minBufferCap) {return new StreamDecoder(ch, dec, minBufferCap);}read是一個線程安全的操作,可以讀取單個字符,也可以讀取多個字符,讀取前要先檢查是否有緩存的字符,如果除了緩存的字符外沒有其他需求則直接返回,否則要使用implRead(char[], int, int)來進行實際的讀取
public int read() throws IOException {return read0();}@SuppressWarnings("fallthrough")private int read0() throws IOException {synchronized (lock) {// 如果緩存中有未返回的字符則直接返回并清空緩存if (haveLeftoverChar) {haveLeftoverChar = false;return leftoverChar;}// Convert more byteschar cb[] = new char[2];int n = read(cb, 0, 2);// 嘗試讀取兩個字符switch (n) {case -1:return -1;// 已到文件結束符返回-1case 2:leftoverChar = cb[1];// 讀取了2個字符,緩存第二個字符haveLeftoverChar = true;// FALL THROUGH繼續進入case1case 1:return cb[0];// 返回第一個字符default:assert false : n;return -1;}}}public int read(char cbuf[], int offset, int length) throws IOException {int off = offset;int len = length;synchronized (lock) {// 同時只能有一個線程進行read操作ensureOpen();// 確保流是打開的if ((off < 0) || (off > cbuf.length) || (len < 0) || ((off + len) > cbuf.length) || ((off + len) < 0)) {throw new IndexOutOfBoundsException();}if (len == 0)return 0;int n = 0;if (haveLeftoverChar) {// 將leftover緩存中的字符復制到數組中cbuf[off] = leftoverChar;off++;len--;haveLeftoverChar = false;n = 1;if ((len == 0) || !implReady())// 如果不需要更多字符或者讀到沒有剩余數據則返回return n;}if (len == 1) {// 只讀一個字符時調用read0,視為讀取兩個緩存一個int c = read0();if (c == -1)return (n == 0) ? -1 : n;cbuf[off] = (char) c;return n + 1;}return n + implRead(cbuf, off, off + len);// 直接調用read()不會進入前兩個if代碼塊,返回實際讀取的字符數}}read方法調用了implRead進行讀取,先通過readBytes將字節盡可能多地讀取到ByteBuffer中,然后通過CharsetDecoder.decode解碼為字符
int implRead(char[] cbuf, int off, int end) throws IOException {//為了處理替代對,這個方法要求調用者試圖讀取至少兩個字符,如果有的話保存多余字符,在更高的層次比在這里更容易處理這個問題。assert (end - off > 1);CharBuffer cb = CharBuffer.wrap(cbuf, off, end - off);if (cb.position() != 0)// Ensure that cb[0] == cbuf[off]cb = cb.slice();boolean eof = false;//decode的第三個參數只有在調用者確保除了buffer中的字節外沒有其他字節了才是truefor (;;) {CoderResult cr = decoder.decode(bb, cb, eof);//將ByteBuffer內的字節解碼存入CharBufferif (cr.isUnderflow()) {//向下溢出,CharBuffer沒有填滿if (eof)break;if (!cb.hasRemaining())break;if ((cb.position() > 0) && !inReady())break; // 最多阻塞一次int n = readBytes();//將字節盡可能多地讀取到ByteBuffer,返回讀取的字節數if (n < 0) {eof = true;//已經到結束符了if ((cb.position() == 0) && (!bb.hasRemaining()))break;decoder.reset();//重置decoder,清除內部狀態}continue;}if (cr.isOverflow()) {//向上溢出,CharBuffer滿了assert cb.position() > 0;break;}cr.throwException();}if (eof) {// ## Need to flush decoderdecoder.reset();}if (cb.position() == 0) {if (eof)return -1;assert false;}return cb.position();}private int readBytes() throws IOException {bb.compact();//使ByteBuffer中的字節變得緊密連接,如果從有字節是在position到limit的位置,把它們復制到頭上去,使得position和capacity保持一致try {if (ch != null) {// 從通道中讀取字節填滿ByteBuffer或者文件中沒有剩余數據int n = ch.read(bb);if (n < 0)return n;} else {// 從流中讀取,更新緩沖區int lim = bb.limit();int pos = bb.position();assert (pos <= lim);//pos>lim會直接拋出異常int rem = (pos <= lim ? lim - pos : 0);//剩余的字節數assert rem > 0;int n = in.read(bb.array(), bb.arrayOffset() + pos, rem);//從InputStream中將全部剩余字節讀取到ByteBuffer直到緩沖區所需的內容裝滿if (n < 0)return n;if (n == 0)throw new IOException("Underlying input stream returned zero bytes");//返回0說明有異常發生,流中沒數據返回的是-1assert (n <= rem) : "n = " + n + ", rem = " + rem;bb.position(pos + n);}} finally {// Flip even when an IOException is thrown,// otherwise the stream will stutterbb.flip();}int rem = bb.remaining();assert (rem != 0) : rem;return rem;}ready方法被重寫了,檢查緩沖區或者文件中是否有可以讀取的數據
public boolean ready() throws IOException {synchronized (lock) {ensureOpen();return haveLeftoverChar || implReady();// 緩存中有字符或者文件中還有剩余數據}}boolean implReady() {return bb.hasRemaining() || inReady();//ByteBuffer中有剩余內容或者輸入流中還有剩余內容}private boolean inReady() {try {return (((in != null) && (in.available() > 0)) || (ch instanceof FileChannel)); // ## RBC.available()?} catch (IOException x) {return false;}}close方法將isOpen設為false,然后關閉ReadableByteChannel或者InputStream,重復調用不會生效。
public void close() throws IOException {synchronized (lock) {if (!isOpen)return;implClose();isOpen = false;}}void implClose() throws IOException {if (ch != null)ch.close();elsein.close();}getChannel獲取文件通道有重復調用失敗立即退出的機制
// 在早期版本中還沒有構建完NIO的native代碼,為了保證第一次嘗試中捕捉到UnsatisfiedLinkError后面再嘗試會立即失敗,所以有了這個標記private static volatile boolean channelsAvailable = true;private static FileChannel getChannel(FileInputStream in) {if (!channelsAvailable)return null;try {return in.getChannel();} catch (UnsatisfiedLinkError x) {channelsAvailable = false;return null;}}總之StreamDecoder就是將讀取到的字節轉換為字符,然后返回。理論上來說除了解碼的具體實現需要依賴底層實現外其他自己重寫應該問題不大,一次至少讀取兩個字符是為了處理替代對。關于用InputStreamReader和使用字節流讀取后再new String轉換成字符哪個比較快有待測試。
總結
以上是生活随笔為你收集整理的Java FileReader InputStreamReader类源码解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HTML5 之 新特性 + 新对象
- 下一篇: week04_python函数返回值、作