阿拉伯人用阿拉伯數字嗎?——記一次用String#format格式化字符串趟到的雷

要生成一個字符串,其中夾雜着一些動態變化的整數,咱們通常是用String.format方法來完成,可是,若是用的不恰當,你多是得不到正確的整數字符串的。java

事情從一個線上崩潰提及,從崩潰堆棧來看,個人一句SQL語句有語法錯誤,執行的時候出錯致使了崩潰。
SQL語句大體的生成以下:git

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吧。bash

好吧,不亂猜了,大不了"read the fuck source code" .app

String#format的源碼如想象的那般簡單,把模式字符串分解成一個數組,每一個數組元素要麼是一個純字符串,要麼是一個'%'符號開頭的格式串,而後遍歷數組,把格式串一個個的替換成target值,再把數組拼接回字符串。
因爲只有整數的轉換出錯,因此重點關注整數的轉換過程,其中一段代碼略顯詭異:ui

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的值。this

char zero = getZero(l); //因爲咱們調用format方法沒有指定locale,因此l=Locale.getDefault();複製代碼

再看getZero方法spa

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。

相關文章
相關標籤/搜索