要生成一個字串,其中夾雜著一些動態變化的整數,我們一般是用String.format方法來完成,但是,如果用的不恰當,你可能是得不到正確的整數字符串的。
事情從一個線上崩潰說起,從崩潰堆疊來看,我的一句SQL語句有語法錯誤,執行的時候出錯導致了崩潰。
SQL語句大致的生成如下:
int i = 0;
String querySql = String.format("select * from table1 where id = %d", i);複製程式碼
完全沒有語法問題的可能,本地執行也是麻溜的通過了。
再細看日誌,原來是format後的SQL語句,%d本該替換為i的值對應的字串,結果卻變成了亂碼,也是導致語法錯誤的原因。看看其他地方的字串格式化,發現只有%d的轉換出了問題,字串的轉換也是%s的轉換是正常的。
所以,String.format在轉換數字的時候,出現了不可靠的一些事情。
JDK裡這麼常用的方法如果不可靠,那肯定是前人踩坑多次,且很有可能還提交過issue了,所以直接上StackOverflow找了一圈,未想到竟沒有結果。
那我只好大膽猜測,莫非是線上某些使用者裝置的字符集是不相容ASCII碼的,所以把數字轉換成了別的字元。這個想法很快被組內一些同事否定了,這世上應該沒有哪個字符集標準傻到不相容ASCII吧。
好吧,不亂猜了,大不了”read the fuck source code” .
String#format的原始碼如想象的那般簡單,把模式字串分解成一個陣列,每個陣列元素要麼是一個純字串,要麼是一個`%`符號開頭的格式串,然後遍歷陣列,把格式串一個個的替換成target值,再把陣列拼接回字串。
由於只有整數的轉換出錯,所以重點關注整數的轉換過程,其中一段程式碼略顯詭異:
char c = value[j];
sb.append((char) ((c - `0`) + zero));複製程式碼
value是整數對應的ASCII碼陣列,比如,整數21對應的value陣列就是[50,49]。按理說,把這個陣列一股腦插入StringBuilder這個例項就萬事大吉了,但是偏偏插入前有一個(char) ((c – `0`) + zero)的轉換過程,把目標字元c減去字元`0`再加上字元zero,看來這一步就是導致轉換出亂子的罪魁禍首了,來看zero的值。
char zero = getZero(l); //由於我們呼叫format方法沒有指定locale,所以l=Locale.getDefault();複製程式碼
再看getZero方法
private char getZero(Locale l) {
if ((l != null) && !l.equals(locale())) {
DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(l);
return dfs.getZeroDigit();
}
return zero;
}複製程式碼
由於locale()方法返回的就是我們傳入的locale,所以這裡不走if,直接返回類屬性zero的值,再看類屬性zero的初始化,是在構造方法裡面通過呼叫靜態方法來賦值的
private static char getZero(Locale l) {
if ((l != null) && !l.equals(Locale.US)) {
DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(l);
return dfs.getZeroDigit();
} else {
return `0`;
}
}複製程式碼
如果locale不是US,我們終究是躲不過上次if語句塊裡的DecimalFormatSymbols的,這個類的例項化很簡單,根據傳入的locale初始化一些固定的值,如小數點符號,分組符號,百分符號,還有我們最關注的zeroDigit
/**
* Gets the character used for zero. Different for Arabic, etc.
*/
public char getZeroDigit() {
return zeroDigit;
}
/**
* Sets the character used for zero. Different for Arabic, etc.
*/
public void setZeroDigit(char zeroDigit) {
this.zeroDigit = zeroDigit;
cachedIcuDFS = null;
}複製程式碼
這兩個方法的方法體不重要,重要的線索在註釋裡:阿拉伯國家的‘0’是不一樣的。至於不一樣在哪裡,把手機語言切換成阿拉伯語,斷點除錯一下,果然有驚喜:String.format(“%d”,0).toCharArray()輸出的字元陣列中,第一個元素值並不是48(對應`0`),而是1632,直接通過String.valueOf((char)1632)轉換為字元,得到一個很粗的‘·’字元,這個應該就是阿拉伯人數字(不是阿拉伯數字)裡面的0了。Google一下,果然如此:
再實驗1633,1634等字元,完全是對應的。同時在我的崩潰日誌裡面出現的亂碼,也正好就是這些東西。
所以,回頭來看(char) ((c – `0`) + zero)這個轉換,就很簡單了。可以看出,String.format對數字的轉換,並不是我們固有的認為是“0變成`0`,1變成`1`”這麼簡單,而是要把“0變成零,1變成一”(打個比方而已,^__^ 嘻嘻……還好我們中國是習慣用123的,所以中文下format並不會出現一二三)。
事情原因就是這麼簡單,解決的辦法自然有了,要麼,呼叫format的時候傳入Locale.US,要麼,別用%d配整數,改用%s配字串。
PS:孟加拉語環境下也有同樣的問題,孟加拉語的0對應Unicode裡面的2534。