要生成一个字符串,其中夹杂着一些动态变化的整数,咱们通常是用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一下,果真如此:
因此,回头来看(char) ((c - '0') + zero)这个转换,就很简单了。能够看出,String.format对数字的转换,并非咱们固有的认为是“0变成'0',1变成'1'”这么简单,而是要把“0变成零,1变成一”(打个比方而已,^__^ 嘻嘻……还好咱中国是习惯用123的,因此中文下format并不会出现一二三)。
事情缘由就是这么简单,解决的办法天然有了,要么,调用format的时候传入Locale.US,要么,别用%d配整数,改用%s配字符串。
PS:孟加拉语环境下也有一样的问题,孟加拉语的0对应Unicode里面的2534。