前幾天面試遇到這個問題:在Java中如何將字符串轉化為整數,當時too young too naive,隨便回答了一下。今天跑去看Java源碼中paresInt函數的寫法,Oh my god!其實不看也能寫出來,但是我覺得源碼中的實現更好。下面貼出源碼順道分析一下:
1 /* @param s the {@code String} containing the integer 2 * representation to be parsed 3 * @param radix the radix to be used while parsing {@code s}. 4 * @return the integer represented by the string argument in the 5 * specified radix. 6 * @exception NumberFormatException if the {@code String} 7 * does not contain a parsable {@code int}. 8 */ 9 public static int parseInt(String s, int radix) 10 throws NumberFormatException 11 { 12 /* 13 * WARNING: This method may be invoked early during VM initialization 14 * before IntegerCache is initialized. Care must be taken to not use 15 * the valueOf method. 16 */ 17 18 if (s == null) { 19 throw new NumberFormatException("null"); 20 } 21 22 if (radix < Character.MIN_RADIX) { 23 throw new NumberFormatException("radix " + radix + 24 " less than Character.MIN_RADIX"); 25 } 26 27 if (radix > Character.MAX_RADIX) { 28 throw new NumberFormatException("radix " + radix + 29 " greater than Character.MAX_RADIX"); 30 } 31 32 int result = 0; 33 boolean negative = false; 34 int i = 0, len = s.length(); 35 int limit = -Integer.MAX_VALUE; 36 int multmin; 37 int digit; 38 39 if (len > 0) { 40 char firstChar = s.charAt(0); 41 if (firstChar < '0') { // Possible leading "+" or "-" 42 if (firstChar == '-') { 43 negative = true; 44 limit = Integer.MIN_VALUE; 45 } else if (firstChar != '+') 46 throw NumberFormatException.forInputString(s); 47 48 if (len == 1) // Cannot have lone "+" or "-" 49 throw NumberFormatException.forInputString(s); 50 i++; 51 } 52 multmin = limit / radix; 53 while (i < len) { 54 // Accumulating negatively avoids surprises near MAX_VALUE 55 digit = Character.digit(s.charAt(i++),radix); 56 if (digit < 0) { 57 throw NumberFormatException.forInputString(s); 58 } 59 if (result < multmin) { 60 throw NumberFormatException.forInputString(s); 61 } 62 result *= radix; 63 if (result < limit + digit) { 64 throw NumberFormatException.forInputString(s); 65 } 66 result -= digit; 67 } 68 } else { 69 throw NumberFormatException.forInputString(s); 70 } 71 return negative ? result : -result; 72 }
首先參數:1)第一個是String,表示需要被轉化的字符串;2)第二個是進制,表示字符串需要當做什么進制的字符串去解析。
18-30行表示:如果是空字符串,或者進制低於能解析的最小進制(2)或者高於能解析的最大進制(36),則拋出異常;
接下來看40-51行:這里主要是根據第一個字符去判斷字符串代表的數字是正的還是負的,通過flag negative標記。
剩余的部分比較復雜,先解釋一下基本思想:取出字符串中的每一位字符,按照進制radix轉化為數字,倘若不是數字,則返回值為-1,拋出異常。到這里都很好理解,包括39行的判斷,都是很基本的。其實我一開始想的是,可以檢測字符串的長度n,然后直接得出最后正數結果相應位置上的數字,大體算法如下:
1 public static int parseInt(String s){ 2 int result = 0; 3 4 int length = s.length(); 5 6 for(int index = 0; index < length; index ++){ 7 int number = s.charAt(index) - '0';//獲取字符代表的數字 8 result += number * Math.pow(10, length - index - 1); 9 } 10 11 return result; 12 }
這里是簡寫,很多情況包括正負都沒有考慮,並且默認是10進制,這是我的想法。但是我發現源碼的想法並非如此,抽象出來大致如下:
1 public static int parseInt(String s){ 2 int result = 0; 3 4 int length = s.length(); 5 6 for(int index = 0; index < length; index ++){ 7 int number = s.charAt(index) - '0'; 8 result *= 10; 9 result += number; 10 } 11 12 return result; 13 }
這樣子寫,減少了很多的乘法,原先在進位上需要做(1+n)n/2次乘法,后面則只需要n次,這是一次改進。
接着代碼要解決的是另外一個很重要的問題,Java中整數值都是32位的,它是有范圍的。我們需要驗證字符串轉化之后是不是在這個范圍以內,即[Integer.MIN_VALUE, Integer.MAX_VALUE]。這就是59-65行要做的事情。
正數最大值可以達到2147483647,如果給出字符串“2147483648”,則解析出來肯定超范圍。如何檢測呢,根據上面的算法,假設解析到214748364,我們打斷解析最后一位,可以通過Integer.MAX_VALUE-214748364 * radius <= 下一個digit來判斷,如果表達式成立,則可以繼續解析,否則不可以解析。但是這樣想是有局限的,比如我們實際要解析的字符串是“89”,則可以看到其實上面那個表達式並不成立,但是89明顯小於最大范圍,可以解析,這里如何解決呢?
我們可以這樣:將范圍同時縮小一個量級,即解析出來的結果不去和2147483647比較,而是和214748364比較,當超出這個范圍的時候,我們再使用上面的表達式進行判斷。負數亦然。按照這個思想,我將上面的代碼改了一下:
1 public static int parseInt(String s){ 2 int result = 0; 3 int limit = Integer.MAX_VALUE; 4 int upLimit = limit / 10; 5 6 int length = s.length(); 7 8 for(int index = 0; index < length; index ++){ 9 int number = s.charAt(index) - '0'; 10 11 if(result > upLimit)//這個時候乘以result * 10,必然大於Integer.MAX_VALUE 12 return -1; 13 14 if(result == upLimit && (limit - result * 10) < number) 15 return -1; 16 17 result *= 10; 18 result += number; 19 } 20 21 return result; 22 }
注意代碼中11行的注釋。我使用幾個邊界數字測試的結果是正確的,這里同樣默認是正數,最大只能解析到2147483647。Java的實現和這個就差不多了,但是Java奇怪的地方是在於使用減法而非加法,可以詳細對比一下Java源碼的66行和上面我的代碼的第18行。Java的這種想法在其代碼的第71行也有表現,我們可以看到,當值是負數的時候,直接返回result,否則是要取負數的。
Java源碼在52行設置了multmin,59-61行代碼和我的代碼的11-12行作用一樣,62-65行則和我代碼的14-15行代碼一樣。但是我的代碼這樣寫,是需要分類討論的,即需要分為正負數去討論。Java的精妙在於:將傳入的字符串去掉正負號,根據正負設定下限,然后使用同一種方法去解析剩余的字符串,而可以不管正負!
在Java的源碼中,如果傳入的是正數,則下限是-Integer.MAX_VALUE,如果是負數,則下限是Integer.MIN_VALUE,然后使用negative去判斷返回的時候是不是應該添加負號。中間則按照上面的思路,59-61行用來確保result * radix不會超出界限,62-65行則用來判斷最終是否超出界限。注意,我的代碼和Java的代碼其實都注意到一點,Java代碼中63行比較符合思維的寫法應該是:
1 if (result - digit < limit) {
但是Java並沒有這樣寫,而是寫成源碼中的形式,我的代碼14行也是如此,這里的主要原因是,這行代碼本身就是檢測result-digit是否超出界限的,如果按照上面的寫法,result-digit如果超出界限,則會報錯,但是按照Java源碼的寫法,limit+digit是肯定在表示范圍內的!
另外,注意,這里並不存在統一設置上限的寫法,因為-Integer.MIN_VALUE > Integer.MAX_VALUE!