JDK原始碼解讀(第五彈:Integer之toString方法)
上一篇只講了Integer的幾個屬性,這一次我們來看一下toString方法。
toString總共有3個過載,先來看兩個引數的toStirng方法:
public static String toString(int i, int radix) {
if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
radix = 10;
/* Use the faster version */
if (radix == 10) {
return toString(i);
}
char buf[] = new char[33];
boolean negative = (i < 0);
int charPos = 32;
if (!negative) {
i = -i;
}
while (i <= -radix) {
buf[charPos--] = digits[-(i % radix)];
i = i / radix;
}
buf[charPos] = digits[-i];
if (negative) {
buf[--charPos] = '-';
}
return new String(buf, charPos, (33 - charPos));
}
這個方法可以按傳入的radix進行toString。
radix指的是進位制數,如果radix不在2-36之間,則看成10進位制。如果radix為10則直接呼叫一個引數的toString方法。接下來的程式碼就是進位制的轉換演算法。我們想想十進位制轉換成其他進位制應該怎麼轉?就是把十進位制數除以進位制數,然後把得到的商繼續除以進位制數直到商小於進位制數為止,然後把最後一步的商拼上剛才每一步得到的餘數(餘數倒過來排列)就是最後結果。
舉個例子,把十進位制的9轉換成二進位制怎麼轉:
1. 9除以2,商為4,餘數為1;
2. 把上一步的商,也就是4,繼續除以2,得到商為2,餘數為0;
3. 把上一步的商,也就是2,繼續除以2,得到商為1,餘數為0;
4. 上一步的商已經小於進位制數2,所以運算結束,可以得到最後的結果了,就是最後一步的商拼上第三步的餘數拼上第二步的餘數拼上第一步的餘數,也就是1001。
所以這段程式碼就是實現了以上的演算法,只要商大於進位制數就迴圈,不斷的除以進位制數,得到餘數後通過digits陣列獲取到餘數對應的字元,每一步的結果都存放在buf數組裡,最後的結果就是這個buf陣列。為了統一運算,而且考慮到整型的範圍中負數的絕對值比正數的絕對值大,避免溢位,所以這裡統一轉成負數的形式來運算。
一個引數的toString方法:
public static String toString(int i) {
if (i == Integer.MIN_VALUE)
return "-2147483648";
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
char[] buf = new char[size];
getChars(i, size, buf);
return new String(buf, true);
}
這個方法裡面用到了stringSize和getChars這兩個方法。stringSize用來求數字的長度,getChars方法是toString的核心。
先來看一下stringSize,這是一個絕妙的方法,相關原始碼:
final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,
99999999, 999999999, Integer.MAX_VALUE };
// Requires positive x
static int stringSize(int x) {
for (int i=0; ; i++)
if (x <= sizeTable[i])
return i+1;
}
先定義了一個sizeTable陣列,這個陣列中存放10個元素,分別對應了一位十進位制數字的最大值(也就是9),二位十進位制數字的最大值(也就是99),三位十進位制數字的最大值(也就是999),四位十進位制數字的最大值(也就是9999),直到十位十進位制數字的最大值,但是十位十進位制數字的最大值不能是999999999,因為int最大值是Integer.MAX_VALUE,也就是2147483647,所以sizeTable陣列最多隻到十位數字且最大值是Integer.MAX_VALUE。
stringSize方法就是利用了上面定義的這個陣列來很巧妙地得到一個int值的長度,只需要幾次比較即可。
不過要注意,sizeTable陣列的元素全是正值,也就是說stringSize方法也就只能求正數的長度,如果是負數,需要先轉成正數再呼叫stringSize方法。
再來看一下getChars方法,這個方法是將一個整數高效地逐位存入一個char陣列中,原始碼如下:
static void getChars(int i, int index, char[] buf) {
int q, r;
int charPos = index;
char sign = 0;
if (i < 0) {
sign = '-';
i = -i;
}
// Generate two digits per iteration
while (i >= 65536) {
q = i / 100;
// really: r = i - (q * 100);
r = i - ((q << 6) + (q << 5) + (q << 2));
i = q;
buf [--charPos] = DigitOnes[r];
buf [--charPos] = DigitTens[r];
}
// Fall thru to fast mode for smaller numbers
// assert(i <= 65536, i);
for (;;) {
q = (i * 52429) >>> (16+3);
r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ...
buf [--charPos] = digits [r];
i = q;
if (i == 0) break;
}
if (sign != 0) {
buf [--charPos] = sign;
}
}
這裡主要是兩個迴圈,先看while迴圈。65536是2的16次方,也就是先處理i的高16位,再處理i的低16位。
剛進while迴圈,第一行程式碼是:
q = i / 100;
這行程式碼的意思很明顯,q是i除以100的商。至於為什麼要除以100,這是因為想把i兩位兩位地處理。
第二行程式碼是:
r = i - ((q << 6) + (q << 5) + (q << 2));
這就有點費解了 ,幸好設計者在這行程式碼上面有行註釋:// really: r = i - (q * 100);
剛才第一行程式碼求得的q是i除以100的商,那麼i - (q * 100)
是什麼呢,不就是i除以100得到的餘數嗎,所以r就是餘數,即r = i % 100。
但是為什麼不直接這麼算而是要用用i - (q * 100)
呢,是因為計算機進行取模運算是比較慢的,所以設計者使用r = i - (q * 100)
來代替r = i % 100
。
但是計算機處理乘法(q * 100)
仍然不夠快,所以設計者使用了移位運算,因為100=64+32+4,也就是100=2的6次方+2的5次方+2的2次方,所以:
q * 100
= q * 2的6次方 + q * 2的5次方 + q * 2的2次方
= q左移6位 + q左移5位 + q左移2位
= (q << 6) + (q << 5) + (q << 2)
於是就得到第二行程式碼:r = i - ((q << 6) + (q << 5) + (q << 2));
再下面就是利用DigitOnes和DigitTens這兩個陣列快速求得r的個位數字和十位數字,然後進行下一次迴圈。
當i < 65536時,就進入下面的for迴圈,也就是開始處理低16位。
進入for迴圈,又是匪夷所思的程式碼:
q = (i * 52429) >>> (16+3);
這個(i * 52429) >>> (16+3)
到底是想幹什麼?
右移16+3也就是右移19位,也就是除以2的19次方,2的19次方=524288,所以
(i * 52429) >>> (16+3)
= (i * 52429) / 2的19次方
= (i * 52429) / 524288
= i * (52429 / 524288)
= i * 0.1000003814697266
= i * 0.1
這個0.1000003814697266的精度已經很多了,就幾乎等於0.1。
所以算了這麼多,這行程式碼其實就是q = i / 10
,還是為了避免乘除法,提高運算效率。
至於為什麼選擇2的19次方524288,是因為52429/524288得到的數字精度在不超出整形範圍內,精度是最高的。
如果選擇2的17次方131072,那麼13108 / 131072 = 0.100006103515625,
或者選擇2的18次方262144,那麼26215 / 262144 = 0.1000022888183594,
或者選擇2的20次方1048576,那麼104858 / 1048576 = 0.1000003814697266,
所得到的精度都不可能更高了。
然後下面的程式碼就很清楚了:
r = i - ((q << 3) + (q << 1));
意思就是r = i-(q*10)
,也就是r等於i除以10得到的餘數。
然後再利用digits陣列得到r對應的字元。
然後繼續迴圈,直到商為0。
分析到這裡,應該就可以想到為什麼要用兩個迴圈,分為高位和低位處理,就是是因為防止高位計算過程當中溢位。
第三個toString:
public String toString() {
return toString(value);
}
注意到上面兩個toString都是staitc的,而這個不是。這個方法就是簡單的呼叫一個引數的toStirng方法。
看完我們發現,JDK的設計者對於效能的重視程度是多麼的變態,為了哪怕是快那麼一丁點兒,也要無所不用其極。下一篇文章我們繼續看Integer的原始碼,接下來的研究會更加驗證這一點。