阿拉伯人用阿拉伯数字吗?——记一次用String#format格式化字符串趟到的雷

要生成一个字符串,其中夹杂着一些动态变化的整数,我们一般是用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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值