前言
22.11.25:事后发现是我蠢了,Java的意思是表示一个数直接用正负号表示符号,用原码表示数值,哪有人用补码转int的…
在做题的过程中,发现了一个神奇的“bug”,Integer.valueOf()对于32位二进制数字符串转化成整型爆出了java.lang.NumberFormatException.forInputString
错误,往下研究了一下,发现问题出在了parseInt()上。
Integer.valueOf()
该函数返回的是整型包装类,其内部调用了Integer.parseInt(String s, int radix)
。于是问题聚焦在这个函数上。
1 | public static Integer valueOf(String s, int radix) throws NumberFormatException { |
parseInt()和parseUnsignedInt()
还原一下问题场景:
1 | // author: SilenceZheng66 |
这就有点迷惑了,明明输入的二进制字符串是32位的(在Integer范围内),为什么会说我格式错误呢?另外,这个parseUnsignedInt()
又是干什么用的呢?带着这样的问题,我们来阅读二者的源码。首先看parseUnsignedInt()
:
1 | /** |
Java1.8中,Character.MIN_RADIX = 2, Character.MAX_RADIX = 36
,也就是说最大支持到36进制。parseUnsignedInt
方法首先要求s
不能带有负号,对于上述问题场景,该方法首先将输入转化为长整型,然后再转回整型输出。由于Java中不存在无符号整型,在整型中,32位二进制数字的第一位必须为符号位,所以返回结果为带符号数-3。再阐述一下,我输入的无符号数 11111111111111111111111111111101 = 4294967293
超出了Java整型的大小限制(2147483647),但没有超过长度限制(32位),于是应该是可以正常表示的。
总之parseUnsignedInt
方法是用于获取无符号数的,它尚且可以根据输入来获得整型,为什么parseInt()
不行呢?我们再来看一下源码:
1 | public static int parseInt(String s, int radix) |
我们可以发现,parseInt
方法首先判断第一个字符是不是正负号,是负号则说明值是负的,否则,值就是正的。这个逻辑在非二进制环境下没有问题,因为非二进制表示的int变量,都会前置负号来表示负数。然而,在二进制数中,并没有所谓的“正负号”概念,数值的正负由符号位表示。所以parseInt()
在转换"11111111111111111111111111111101"
时将符号位也当做实际的值计算进去了,导致了数值溢出报错。
一种解决办法是将首位数字改为正负号,可以运行成功,但丧失了原本的意义,为什么这么说呢?请看下例:
1 | // author: SilenceZheng66 |
b的输出竟然是-2147483645,并不是想要表达的真实含义(-3),这是因为parseInt()
首先将"-1111111111111111111111111111101"
拆分为 "-"和"01111111111111111111111111111101"
,其中后者表示的真值为2147483645,然后再把负号放进来组合成了输出,这显然与补码"11111111111111111111111111111101"
的真值不一致。
综上,在处理32位补码时,需要采用parseUnsignedInt()
。其实感觉这个parseInt()
可以优化一下,对32位二进制数用首位数字判断符号就好了。
后记
首发于 silencezheng.top,转载请注明出处。