JDK源碼註釋中將InputStreamReader在Java IO中的角色定義爲鏈接字節流和字符流的橋樑,它使用指定的編碼方式Charset將輸入的的字節數據解碼爲字符數據,編碼方式可能基於編碼名稱或者被明肯定義,或者使用平臺默認的編碼方式。每次調用read方法都會致使底層字節流一個或者多個字節數據被讀取進來,爲了提高字節到字符數據的轉化效率,可能一次性從底層字節流讀取超出所需的字節數據。爲了更高的性能,能夠考慮自外層包裹一個BufferedReader類。java
public class InputStreamReader extends Reader
InputStreamReader繼承自Reader類,支持字符流Reader提供的一些基本操做數組
public class InputStreamReader extends Reader { //InputStreamReader的功能依賴於此類實現,稍後源碼分析咱們會講解到這個類 private final StreamDecoder sd; }
/** * 構造函數,使用默認的編碼方式 */ public InputStreamReader(InputStream in) { super(in); try { sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object } catch (UnsupportedEncodingException e) { // The default encoding should always be available throw new Error(e); } } /** * 構造函數,使用指定名稱的編碼方式。若指定名稱的編碼方式不支持或者不存在拋出UnsupportedEncodingException異 * 常,支持的編碼方式可查看java.nio.charset.Charset charset類 */ public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException { super(in); if (charsetName == null) throw new NullPointerException("charsetName"); sd = StreamDecoder.forInputStreamReader(in, this, charsetName); } /** * Creates an InputStreamReader that uses the given charset. * * @param in An InputStream * @param cs A charset * * @since 1.4 * @spec JSR-51 */ public InputStreamReader(InputStream in, Charset cs) { super(in); if (cs == null) throw new NullPointerException("charset"); sd = StreamDecoder.forInputStreamReader(in, this, cs); } /** * 構造函數,使用指定的CharsetDecoder(decoder)對象解碼 */ public InputStreamReader(InputStream in, CharsetDecoder dec) { super(in); if (dec == null) throw new NullPointerException("charset decoder"); sd = StreamDecoder.forInputStreamReader(in, this, dec); }
三個構造函數邏輯大致一致,咱們選擇InputStreamReader(InputStream in, String charsetName)這個方法分析下InputStream構造函數的大致流程。緩存
首先看下上述方法源碼,可知它第一步調用父類構造方法super(in),具體作了什麼咱們直接進入該方法markdown
protected Reader(Object lock) { if (lock == null) { throw new NullPointerException(); } this.lock = lock; }
看到這裏大部分人應該知道他其實是指定構造方法傳入的底層字節流in做爲同步代碼塊鎖對象。數據結構
繼續回到構造函數往下分析,接下來判斷傳入的編碼方式名稱字符串的是否爲null,再下來調用StreamDecoder.forInputStreamReader(in, this, charsetName)咱們繼續進入該方法的內部源碼:ide
public static StreamDecoder forInputStreamReader(InputStream var0, Object var1, CharsetDecoder var2) { return new StreamDecoder(var0, var1, var2); }
方法內部調用構造方法建立了一個StreamDecoder對象,並讓InputStreamReader內部的sd引用指向它。咱們這裏查看下StreamDecoder的類結構和它的構造函數的源碼:函數
package sun.nio.cs; public class StreamDecoder extends Reader { private static final int MIN_BYTE_BUFFER_SIZE = 32; private static final int DEFAULT_BYTE_BUFFER_SIZE = 8192; private volatile boolean isOpen; private boolean haveLeftoverChar; private char leftoverChar; private static volatile boolean channelsAvailable = true; private Charset cs; private CharsetDecoder decoder; private ByteBuffer bb; private InputStream in; private ReadableByteChannel ch; StreamDecoder(InputStream var1, Object var2, CharsetDecoder var3) { //將內部鎖對象lock設置爲var2即上一步傳入的InputStreamReader對象 super(var2); //更新狀態 this.isOpen = true; //是否存在多解析的一個字符 this.haveLeftoverChar = false; //獲取對應的編碼方式 this.cs = var3.charset(); this.decoder = var3; if (this.ch == null) { this.in = var1; this.ch = null; this.bb = ByteBuffer.allocate(8192); } //移動position位置,清空數據,重複利用緩衝區 this.bb.flip(); } }
分析StreamDecoder源碼咱們知道他也繼承自Reader,這裏綁定的StreamCoder構造函數主要是作了一些初始化操做,包括指定它所屬的InputStreamReader爲鎖對象,綁定了底層操做的字節流in,更新流狀態,分配一個初始容量8192的數組用於緩存從底層字節流in讀取的字節數據。oop
總的來講InputStreamReader的構造函數其實就作了一件事,基於指定底層字節輸入流in和編碼方式初始化了該類所綁定的StreamDecoder對象,該對象是InputStreamReader的核心,InputStreamReader的全部讀取轉化操做都是委託給該類對象完成的。源碼分析
public int read() throws IOException { return sd.read(); }
內部調用sd.read(),跟進StreamDecoder的read()方法性能
private int read0() throws IOException { Object var1 = this.lock; /**注意這裏是對所屬的InputStreamReader對象加鎖,若是有多個線程操做該InputStreamReader對象 * 綁定的底層字節流目標,對象例如硬盤保存的某個文件數據仍是會有數據不一致問題 */ synchronized(this.lock) { if (this.haveLeftoverChar) { this.haveLeftoverChar = false; return this.leftoverChar; } else { char[] var2 = new char[2]; //一次讀取解析兩個字符 int var3 = this.read(var2, 0, 2); switch(var3) { case -1: return -1; case 0: default: assert false : var3; return -1; case 2: this.leftoverChar = var2[1]; this.haveLeftoverChar = true; case 1: return var2[0]; } } } }
繼續跟進read(char[] var1, int var2, int var3)方法源碼:
public int read(char[] var1, int var2, int var3) throws IOException { int var4 = var2; int var5 = var3; Object var6 = this.lock; synchronized(this.lock) { this.ensureOpen(); if (var4 >= 0 && var4 <= var1.length && var5 >= 0 && var4 + var5 <= var1.length && var4 + var5 >= 0) { if (var5 == 0) { return 0; } else { byte var7 = 0; if (this.haveLeftoverChar) { var1[var4] = this.leftoverChar; ++var4; --var5; this.haveLeftoverChar = false; var7 = 1; if (var5 == 0 || !this.implReady()) { return var7; } } //若是有以前保留的未被讀取的解析字符,繼續從字節流讀取解析下一個字符 if (var5 == 1) { int var8 = this.read0(); if (var8 == -1) { return var7 == 0 ? -1 : var7; } else { var1[var4] = (char)var8; return var7 + 1; } } else {//不然讀取解析兩個字符,這是真正進行字節流讀取轉化爲字符的地方 return var7 + this.implRead(var1, var4, var4 + var5); } } } else { throw new IndexOutOfBoundsException(); } } }
前面的零碎邊緣邏輯直接略過咱們直接進入StreamDecoder.implRead方法源碼:
int implRead(char[] var1, int var2, int var3) throws IOException { assert var3 - var2 > 1; //將保存解析後的字符數據的數組封裝到一個CharBuffer對象 CharBuffer var4 = CharBuffer.wrap(var1, var2, var3 - var2); if (var4.position() != 0) { var4 = var4.slice(); } boolean var5 = false; while(true) { //當字節流緩存bb和保存解析後字符數組的CharBuffer都準備好,decoder.decode方法真正負責字符解析 CoderResult var6 = this.decoder.decode(this.bb, var4, var5); if (var6.isUnderflow()) { if (var5 || !var4.hasRemaining() || var4.position() > 0 && !this.inReady()) { break; } //若是未到位字節流尾部繼續讀取字節流到字節流緩存bb中 int var7 = this.readBytes(); /** 省略後續代碼 **/ } } }
看到這裏同窗們可能已經知道最終字節數據解碼爲字符是由StreamDecode內部的成員變量decoder完成的,decoder基於建立InputStreamReader時構造函數傳入的編碼方式選擇對應的decoder,這個decoder位於Charset子類的內部類中,若是不指定則按照系統默認的編碼方式對應的decoder進行解碼。這部分核心邏輯是在JDK中java.nio.charset.Charset類的lookup方法中,內部源碼以下:
private static Charset lookup(String charsetName) { if (charsetName == null) throw new IllegalArgumentException("Null charset name"); Object[] a; if ((a = cache1) != null && charsetName.equals(a[0])) return (Charset)a[1]; return lookup2(charsetName); }
查看源碼咱們可知,它內部建立了一個長度爲2的對象數組緩存了一個第一優先級的編碼名稱到編碼方式Charset的映射,由於JDK認爲大部分狀況下程序使用的編碼方式時相同的這樣能夠提高性能,基於編碼名稱獲取對應編碼方式類Charset的源碼是在這個lookup2方法內,咱們進入方法源碼看下:
private static Charset lookup2(String charsetName) { Object[] a; if ((a = cache2) != null && charsetName.equals(a[0])) { cache2 = cache1; cache1 = a; return (Charset)a[1]; } Charset cs; if ((cs = standardProvider.charsetForName(charsetName)) != null || (cs = lookupExtendedCharset(charsetName)) != null || (cs = lookupViaProviders(charsetName)) != null) { cache(charsetName, cs); return cs; } /* Only need to check the name if we didn't find a charset for it */ checkName(charsetName); return null; }
在這個方法裏面咱們發現Charset內部還使用了一樣長度爲2的Object數組緩存了一個charsetName編碼名稱和Charset對象的映射,這個做爲系統使用編碼方式Charset的第2優先級緩存,若是第2優先級緩存命中編碼名稱那麼,第1第2優先級緩存位置對換,不然真正開始基於charsetName獲取Charset對象,這裏依照前後順序查詢standardProvider(標準編碼)、lookupExtendedCharset(擴展編碼)、lookupViaProviders(自定義編碼)獲取匹配名稱的Charset,咱們選擇標準編碼方式分析下,這裏standardProvider的類是StandardCharsets,方法是在父類FastCharsetProvider中實現的,進入該類對應方法源碼:
public final Charset charsetForName(String var1) { synchronized(this) { return this.lookup(this.canonicalize(var1)); } }
這裏方法內部先調用canonicalize方法基於傳入的charsetName去查詢內部維護的編碼名稱別名映射aliasMap若是是別名則使用對應的編碼名稱替換別名,而後將此編碼名稱字符串做爲參數調用了本類FastCharsetProvider的lookup方法,繼續進入該方法源碼:
private Charset lookup(String charsetName) { String csn = canonicalize(toLower(charsetName)); ...... String cln = classMap.get(csn); ...... try { Class<?> c = Class.forName(packagePrefix + "." + cln, true, this.getClass().getClassLoader()); cs = (Charset)c.newInstance(); cache.put(csn, cs); return cs; } catch (ClassNotFoundException | IllegalAccessException | InstantiationException x) { return null; } }
在源碼中能夠看到,在方法開頭對傳入的charsetName先進行了轉小寫操做,好比咱們以前傳入的是UTF-8,那麼代碼中獲得的csn就是utf-8,這代表了咱們在調用InputStreamReader不須要區分大小寫,JDK內部會統一轉換爲小寫,不過最好統一使用小寫,緣由參考前面Charset第1第2級緩存的講解。這裏方法內部又進行了一次別名轉化操做,多是爲了防止上一次調用方方法傳入charsetName與aliasMap(保存別名映射的容器)中大小寫匹配失敗的狀況,接着方法基於轉化後的charsetName到classMap中去找對應的class類,classMap中是保存了編碼方式(字符集)名稱如utf-8到對應class類的映射如UTF_8。最後拼湊出對應的Charset類名稱包含全路徑,如sun.nio.cs.UTF_8,經過Class.forName方法去加載類,調用Class的newInstance方法實例化一個UTF_8對象,強制轉化爲Charset並緩存到cache。
到了這裏decoder對象如何獲取的問題已經很清晰了,StreamDecoder內部負責字節解碼的成員變量decoder實例就是由以前步驟獲取到的Charset對象的newDecoder方法返回的,在類UTF_8中它是直接返回內部類Decoder。
有了decoder咱們能夠繼續回到前面的implRead方法源碼繼續往下分析字符的讀取。
int implRead(char[] var1, int var2, int var3) throws IOException { assert var3 - var2 > 1; //將保存解析後的字符數據的數組封裝到一個CharBuffer對象 CharBuffer var4 = CharBuffer.wrap(var1, var2, var3 - var2); if (var4.position() != 0) { var4 = var4.slice(); } boolean var5 = false; while(true) { //當字節流緩存bb和保存解析後字符數組的CharBuffer都準備好,decoder.decode方法真正負責字符解析 CoderResult var6 = this.decoder.decode(this.bb, var4, var5); if (var6.isUnderflow()) { if (var5 || !var4.hasRemaining() || var4.position() > 0 && !this.inReady()) { break; } //若是未到位字節流尾部繼續讀取字節流到字節流緩存bb中 int var7 = this.readBytes(); /** 省略後續代碼 **/ } } }
這裏方法就作了兩件事:1將InputStream中的數據讀取到ByteBuffer中,也就是bb中;2將ByteBuffer也就是bb中的數據經過CharsetDecoder的decode方法不斷解碼到CharBuffer也就是cb中。
對於第1件事他是經過this.readBytes()方法完成的,進入該方法
private int readBytes() throws IOException { ...... int var4 = this.in.read(this.bb.array(), this.bb.arrayOffset() + var2, var3); if (var4 < 0) { int var5 = var4; return var5; } ...... }
如代碼所示內部就是經過內部綁定的InputStream讀取字節到待解析的字節緩衝區bb中,這個InputStream是在InputStreamReader建立時綁定到內部的成員變量StreamReader中的,這樣InputStream、StreamReader和InputStreamReader的關係就很清晰了。
第2件事情,也就是對ByteBuffer(即成員變量bb)中的字節進行解碼並保存到字符緩衝區CharBuffer中,是經過CharsetDecoder中的decode方法來完成的,此方法接收三個參數,其中前兩個分別是保存待解碼字節數據的字節緩衝區和用來存放解碼後數據的字符緩衝區。這裏咱們選取UTF-8字符集編碼方式進行分析,UTF-8負責解碼的方法decodeLoop位於UTF_8類的內部Decoder類,咱們進入該方法
protected CoderResult decodeLoop(ByteBuffer var1, CharBuffer var2) { return var1.hasArray() && var2.hasArray() ? this.decodeArrayLoop(var1, var2) : this.decodeBufferLoop(var1, var2); }
咱們這邊的ByteBuffer和CharBuffer都支持數組,所以進入decodeArrayloop方法
private CoderResult decodeArrayLoop(ByteBuffer var1, CharBuffer var2) { byte[] var3 = var1.array(); int var4 = var1.arrayOffset() + var1.position(); int var5 = var1.arrayOffset() + var1.limit(); char[] var6 = var2.array(); int var7 = var2.arrayOffset() + var2.position(); int var8 = var2.arrayOffset() + var2.limit(); for(int var9 = var7 + Math.min(var5 - var4, var8 - var7); var7 < var9 && var3[var4] >= 0; var6[var7++] = (char)var3[var4++]) { ; } while(true) { while(var4 < var5) { byte var10 = var3[var4]; //多字節 if (var10 < 0) { //雙字節字符 格式 110xxxxx 10xxxxxx if (var10 >> 5 == -2 && (var10 & 30) != 0) { if (var5 - var4 < 2 || var7 >= var8) { return xflow(var1, var4, var5, var2, var7, 2); } byte var17 = var3[var4 + 1]; if (isNotContinuation(var17)) { return malformedForLength(var1, var4, var2, var7, 1); } var6[var7++] = (char)(var10 << 6 ^ var17 ^ 3968); var4 += 2; } else { int var11; byte var12; byte var13; //三字節 格式 1110xxxx 10xxxxxx 10xxxxxx if (var10 >> 4 == -2) { var11 = var5 - var4; if (var11 < 3 || var7 >= var8) { if (var11 > 1 && isMalformed3_2(var10, var3[var4 + 1])) { return malformedForLength(var1, var4, var2, var7, 1); } return xflow(var1, var4, var5, var2, var7, 3); } var12 = var3[var4 + 1]; var13 = var3[var4 + 2]; if (isMalformed3(var10, var12, var13)) { return malformed(var1, var4, var2, var7, 3); } char var18 = (char)(var10 << 12 ^ var12 << 6 ^ var13 ^ -123008); if (Character.isSurrogate(var18)) { return malformedForLength(var1, var4, var2, var7, 3); } var6[var7++] = var18; var4 += 3; } else { if (var10 >> 3 != -2) { return malformed(var1, var4, var2, var7, 1); } //四字節,格式1111xxxx 10xxxxxx 10xxxxxx 10xxxxxx var11 = var5 - var4; if (var11 >= 4 && var8 - var7 >= 2) { var12 = var3[var4 + 1]; var13 = var3[var4 + 2]; byte var14 = var3[var4 + 3]; int var15 = var10 << 18 ^ var12 << 12 ^ var13 << 6 ^ var14 ^ 3678080; if (!isMalformed4(var12, var13, var14) && Character.isSupplementaryCodePoint(var15)) { var6[var7++] = Character.highSurrogate(var15); var6[var7++] = Character.lowSurrogate(var15); var4 += 4; continue; } return malformed(var1, var4, var2, var7, 4); } int var16 = var10 & 255; if (var16 <= 244 && (var11 <= 1 || !isMalformed4_2(var16, var3[var4 + 1] & 255))) { if (var11 > 2 && isMalformed4_3(var3[var4 + 2])) { return malformedForLength(var1, var4, var2, var7, 2); } return xflow(var1, var4, var5, var2, var7, 4); } return malformedForLength(var1, var4, var2, var7, 1); } } } else {//單字節 格式0xxxxxxx if (var7 >= var8) { return xflow(var1, var4, var5, var2, var7, 1); } var6[var7++] = (char)var10; ++var4; } } return xflow(var1, var4, var5, var2, var7, 0); } }
因爲方法涉及到UTF-8編碼咱們先來簡單瞭解下UTF-8編碼存儲方式
Unicode編碼和UTF-8編碼
首先Unicode是一個符號集,它只規定了符號的二進制代碼,卻沒有規定這個二進制代碼應該如何存儲。而UTF-8則是對Unicode的一種實現方式。
UTF-8 最大的一個特色,就是它是一種變長的編碼方式。它可使用1~4個字節表示一個符號,根據不一樣的符號而變化字節長度。
UTF-8 的編碼規則很簡單,只有二條:
1)對於單字節的符號,字節的第一位設爲0,後面7位爲這個符號的 Unicode 碼。所以對於英語字母,UTF-8 編碼和 ASCII 碼是相同的。 2)對於n字節的符號(n > 1),第一個字節的前n位都設爲1,第n + 1位設爲0,後面字節的前兩位一概設爲10。剩下的沒有說起的二進制位,所有爲這個符號的 Unicode 碼。
下表總結了編碼規則,字母x表示可用編碼的位:
Unicode符號範圍 | UTF-8編碼方式(二進制) |
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
根據上表,很容易可以判斷出一個UTF-8編碼的字符佔用幾個字節。若是前面是一個0,則此字符只佔用一個字節;若是第一位是1,則連續有多少個1,就表示當前字符佔用多少個字節。
下面,仍是以漢字嚴爲例,講解如何實現 UTF-8 編碼:
嚴的 Unicode 是4E25(100111000100101),根據上表,能夠發現4E25處在第三行的範圍內(0000 0800 - 0000 FFFF),所以嚴的 UTF-8 編碼須要三個字節,即格式是1110xxxx 10xxxxxx 10xxxxxx。而後,從嚴的最後一個二進制位開始,依次從後向前填入格式中的x,多出的位補0。這樣就獲得了,嚴的 UTF-8 編碼是11100100 10111000 10100101,轉換成十六進制就是E4B8A5。
下面基礎知識講完咱們繼續回到decodeLoop方法,方法較長所以我將非核心邏輯省略掉了。
查看源碼,decodeLoop在方法開始時,會將ByteBuffer中的數據轉爲數組並保存到sa中,也就是說sa中包存了全部的從InputStream中讀取到的byte數據;同時會將CharBuffer中的數據轉爲數組並保存到da中,也就是說轉碼後的數據都將保存到da中,因爲採用的是數組引用,所以數據最終是保存在咱們文章開始聲明的char數組當中的(CharBUffer也是對char數組的包裹)。
以後咱們把注意力重點放在while循環中,進入循環體中,首先讀取一個字節存放到b1中,以後經過if-else語句來判斷當前字符采用UTF-8編碼方式佔用了幾個字節,判斷的邏輯很是簡單,咱們前面提到UTF-8是變長存儲的,能夠佔用1-4個字節,參考前面UTF-8對於佔用不一樣字節字符的編碼規則很容易得出如下解碼流程:
1)假設當前字符佔用一個字節,那麼讀取出的var10的格式就是0xxxxxxx,而咱們知道以0開頭的二進制數爲正數,1開頭的爲負數,所以若是var10大於0,則當前字符確定使用一個字節存儲。這正是第一個if語句的邏輯,若是隻佔用一個字節,則將var10轉爲char,保存到CharBuffer內部的數組中。
2)假設當前字符佔用兩個字節,那麼讀取出的var10的格式就是110xxxxx,由於此數據後面五位都是數據位,所以向右移動五位,獲得11111110。將此數據轉爲十進制就是-2(負數二進制是採用補碼錶示的,所以須要轉到原碼,也就是:10000010)。若是當前字符佔用兩個字節,則從ByteBuffer中再讀取一個字節,存放到var17中,以後經過解碼策略將var10和var17解碼爲char類型存放到CharBuffer內部的數組中。
3)假設當前字符佔用三個字節,那麼讀取出的var0的格式就是1110xxxx,由於此數據後面四位都是數據位,所以向右移動四位,獲得11111110。將此數據轉爲十進制就是-2(負數二進制是採用補碼錶示的,所以須要轉到原碼,也就是:10000010)。若是當前字符佔用三個字節,則從ByteBuffer中再讀取兩個字節,分別存放到var12和var13中,以後經過解碼策略將var十、var12和var13解碼爲char類型存放到CharBuffer中
4)假設當前字符佔用四個字節,那麼讀取出的var10的格式就是11110xxx,由於此數據後面三位都是數據位,所以向右移動三位,獲得11111110。將此數據轉爲十進制就是-2(負數二進制是採用補碼錶示的,所以須要轉到原碼,也就是:10000010)。若是當前字符佔用四個字節,則從ByteBuffer中再讀取三個字節,分別存放到var十、var十二、var13和var14中,以後經過解碼策略將var十、var十二、var13和var14解碼爲char類型存放到CharBuffer中
以上就是整個解碼過程,經過以上過程最終將讀取到的字節轉換爲字符保存到了字符數組中。
總結read方法作的事情有:1.基於指定編碼方式對應的Charset提供的decoder將字節流解碼爲字符,一次解析兩個字符,返回一個,另外一個字符緩存於綁定StreamDecoder對象的成員變量lazeOverChar,經過這種方式提高read()方法的查詢速度。