有关数值的小细节
数字运算看着简单,实际不留神会踩很多坑。。。
数值字面量与赋值
浮点默认是double
1 | float a = 1.2; //编译错误 可以改成1.2f |
整数默认是int
,只能装箱成Integer
1 | long l1 = 1; //原生类型可不带标识 |
作为参数传递时,long性质和声明行为一致
但是short作参数时,必须强转
1 | primitiveLong(1); //primitiveLong(long num) |
数值常量
各个数值类都有最大最小值常量
1 | public static final int MIN_VALUE = 0x80000000; |
有时操作常用数字,字面量容易造成magic number的感觉,有现成的常量更佳
commons-lang3
包的NumberUtils
提供-1,0,1的各类型常量
1 | public static final Long LONG_ZERO = Long.valueOf(0L); |
溢出
超过范围,直接截断
整型最大值Integer.MAX_VALUE
为2147483647
1 | System.out.println(Integer.MAX_VALUE * Integer.MAX_VALUE); //溢出,结果为(111...000...01),截断后刚好为1 |
Long最大值可以容纳Integer最大值的平方,通常可以使用Long来应对整型溢出
小范围转大范围
char
和short
同为两字节,但char
的内部机制是无符号的
用-1来验证,其二进制表示所有位全为1,赋值给整型后自动符号扩展
1 | char c = (char) -1; |
除了char
以外,其他都默认执行有符号扩展,如果想实现无符号扩展,可以通过位运算截断
1 | byte b = -1; //全 |
大范围转小范围
比如long
转int
,直接强制转有溢出风险
Java8
1 | long longNum = 1L; |
内部加入了溢出检测逻辑
1 | public static int toIntExact(long value) { |
数值计算范围提升
小于int的整数相加都自动转成int
1 | byte a = 1; |
小于int的类型不应该使用复合赋值,因为它会强制截断,可能导致意外结果
下面程序永远不会终止,因为short
先会提升成int
,无符号右移移位后,低位仍然全是1,截断后无变化
1 | short i = -1; |
两个int运算,结果依然是int,先溢出再赋值
1 | int a = Integer.MAX_VALUE; |
char类型计算后同样会变成int
1 | char lower = 'e'; |
浮点本身精度损失
浮点数不能精确计算,有精度损失,计算结果不能拿来比较
小数转2进制是乘2取整,0.99表示成2进制位数无限,而浮点数位有限,必定损失
也就是说字面上把0.99赋给变量,输出变量,呈现的也是0.99,但这只是表面
实际底层进行了有效数字截断,真实值只是最接近于0.99的浮点值
进行运算后,有可能把损失的精度暴露出来
1 | double a = 1; |
整数转浮点精度损失
即使浮点数double范围比long大,但是转换浮点后还是会有精度损失
因为double内部的有效数字位53位,而long是64位
类似float内部的有效数字位24位,int是32位同样会出现精度损失
可以想象double
和long
同为64位,但是存储方式截然不同,因此double
为了追求更大的范围并不能无损覆盖long
1 | long x = Long.MAX_VALUE; |
也就是浮点数足够大,造成精度损失后,变化1不足以让浮点改变
ulp
函数可以检测浮点数间的差距
1 | float a = 200000000f; |
除0
普通的整数除以0,会报java.lang.ArithmeticException: / by zero
然而浮点数是可以除0的,Double
类就定义了特殊的变量
1 | public static final double POSITIVE_INFINITY = 1.0 / 0.0; |
因为无穷有正负之分,因此0和-0也有点小区别
1 | System.out.println(1 / 0d); //Infinity |
0还可以除0
1 | public static final double NaN = 0.0d / 0.0; |
一切由NAN
参与的运算结果都是NAN
,涉及到NAN
的判定都是false
数值比较
如果类型不匹配,可能发生意外
如果采用equals
方法,会走装箱策略,变成两个对象比较,由于类型不同,直接不相等
如果采用==
方法,会走拆箱策略变成数值比较
1 | short num = 1; |
还要注意拆箱过程中null影响
1 | Integer a = null; |
使用Java7提供的对象比较处理
1 | Integer a = null; |
典型的场景是Map, 它是通过equal判定命中
如果定义了Map<Long,Object>,那么取值时一定要注意类型
1 | Map<Long, Object> map = new HashMap<>(); |
如果确定是同种数字比较的话,可以借助字符串比较
1 | public static <T extends Number> boolean compareMix(T num1, T num2) { |
特殊的最小值
整数范围并不是对称的,最小值Integer.MIN_VALUE
为-2147483648
,而最大值Integer.MAX_VALUE
为2147483647
,也就是负数的范围比正数要大
Integer.MIN_VALUE
是个比较特殊的值,二进制表示是0x80000000
,对这个数取负数0x7fffffff + 1
依然是本身,实际上是溢出了。而java本身不考虑溢出,会产生如下诡异的结果
1 | boolean res = Integer.MIN_VALUE == -Integer.MIN_VALUE; //true |
0x7fffffff + 1
可以看成最大再加1,即最大值增加后会回到最小值
1 | boolean res = Integer.MAX_VALUE + 1 == Integer.MIN_VALUE; //true |
因为正负范围不对等,因此if(num < 0) num = -num;
是不对的,没有考虑到极限情况
绝对值函数遇到Integer.MIN_VALUE
会原样返回,因此可能是负值
1 | int abs = Math.abs(Integer.MIN_VALUE); //-2147483648 |
最小值再减1也会变成最大值
1 | boolean res = Integer.MIN_VALUE - 1 == Integer.MAX_VALUE; //true |
运算优先级
取余%
和乘*
具有相同的优先级
1 | int ex1 = 2 % 10 * 10; |
移位运算
移位是取余的,对于整数移位32位会被认为等同于移0位
因此Java中无法把一个数通过一次移位置0
1 | int i = -1; |
取余运算
Java的取余运算支持负数。。结果符号和最左边操作数一致
绝大多数情况并不需要负数,应考虑排除
1 | System.out.println(5 % 2); //1 |
三目运算
三目运算不完全等同于if-else,还有隐式的类型转换
如果比较的两者类型不等,并且其中包含装箱类型,那么会先拆箱成基本类型,然后转成较大的类型
1 | Object a = true ? Integer.valueOf(1) : Double.valueOf(1.2); //1.0 |
三目运算最好避免不同类型混合
总结
- 字面数字默认是int和double
- 考虑拆装箱影响
- 数字比较大考虑溢出
- 避免不同类型数值混合计算
版权声明
This site by Linest is licensed under a Creative Commons BY-NC-ND 4.0 International License.
由Linest创作并维护的博客采用创作共用保留署名-非商业-禁止演绎4.0国际许可证。