Java 数值与计算

有关数值的小细节

数字运算看着简单,实际不留神会踩很多坑。。。

数值字面量与赋值


浮点默认是double

Sample
1
2
float a = 1.2;  //编译错误 可以改成1.2f
double b = 1.2d; //double也可以显式加上后缀d

整数默认是int,只能装箱成Integer

Sample
1
2
3
4
5
6
long l1 = 1; //原生类型可不带标识
Long l2 = 1L; //对象型必须加后缀L,否则装箱成Integer,类型不匹配
short c1 = 1;
Short c2 = 1; //hmm...竟然不报错,这就是short没有规定后缀的原因?
byte b1 = 1;
Byte b2 = 1; //也没有后缀

作为参数传递时,long性质和声明行为一致
但是short作参数时,必须强转

Sample
1
2
3
4
primitiveLong(1); //primitiveLong(long num)
objectLong(1L); //objectLong(Long num)
primitiveShort((short)1); //primitiveShort(short num)
objectShort((short)1); //objectShort(Short num)

数值常量


各个数值类都有最大最小值常量

Integer.java
1
2
public static final int   MIN_VALUE = 0x80000000;
public static final int MAX_VALUE = 0x7fffffff;

有时操作常用数字,字面量容易造成magic number的感觉,有现成的常量更佳
commons-lang3包的NumberUtils提供-1,0,1的各类型常量

NumberUtils.java
1
2
3
public static final Long LONG_ZERO = Long.valueOf(0L);
public static final Long LONG_ONE = Long.valueOf(1L);
public static final Long LONG_MINUS_ONE = Long.valueOf(-1L);

溢出


超过范围,直接截断
整型最大值Integer.MAX_VALUE2147483647

Sample
1
System.out.println(Integer.MAX_VALUE * Integer.MAX_VALUE); //溢出,结果为(111...000...01),截断后刚好为1

Long最大值可以容纳Integer最大值的平方,通常可以使用Long来应对整型溢出

小范围转大范围


charshort同为两字节,但char的内部机制是无符号的
用-1来验证,其二进制表示所有位全为1,赋值给整型后自动符号扩展

Sample
1
2
3
4
5
6
7
char c = (char) -1;
int i1 = c;
System.out.println(i1); //65535

short s = -1;
int i2 = s;
System.out.println(i2); //-1

除了char以外,其他都默认执行有符号扩展,如果想实现无符号扩展,可以通过位运算截断

Sample
1
2
3
4
5
byte b = -1; //全
int i = b;
System.out.println(i); //-1
i = b & 0xff;
System.out.println(i); //255

大范围转小范围


比如longint,直接强制转有溢出风险
Java8

Sample
1
2
long longNum = 1L;
int intNum = Math.toIntExact(longNum);

内部加入了溢出检测逻辑

Math.java
1
2
3
4
5
6
public static int toIntExact(long value) {
if ((int)value != value) {
throw new ArithmeticException("integer overflow");
}
return (int)value;
}

数值计算范围提升


小于int的整数相加都自动转成int

Sample
1
2
3
4
byte a = 1;
short b = 1;
b = a + b; //编译错误,结果已变成int,不能再赋值给short类型
b += a; //+=比较特殊,隐含了强制转换,相当于b=(short)(b+a)

小于int的类型不应该使用复合赋值,因为它会强制截断,可能导致意外结果
下面程序永远不会终止,因为short先会提升成int,无符号右移移位后,低位仍然全是1,截断后无变化

Sample
1
2
3
4
short i = -1;
while( i != 0) {
i >>>= 1
}

两个int运算,结果依然是int,先溢出再赋值

Sample
1
2
3
int a = Integer.MAX_VALUE;
int b = 1;
long c = a + b; //看上去c完全可以容纳和值,但结果c是负数,因为和已经溢出

char类型计算后同样会变成int

Sample
1
2
char lower = 'e';
char upper = (char)(lower - 'a' + 'A');

浮点本身精度损失


浮点数不能精确计算,有精度损失,计算结果不能拿来比较
小数转2进制是乘2取整,0.99表示成2进制位数无限,而浮点数位有限,必定损失
也就是说字面上把0.99赋给变量,输出变量,呈现的也是0.99,但这只是表面
实际底层进行了有效数字截断,真实值只是最接近于0.99的浮点值
进行运算后,有可能把损失的精度暴露出来

Sample
1
2
3
4
5
6
double a = 1;
double b = 0.99;
double c = 0.01;
System.out.println(a - b); //0.010000000000000009
System.out.println(c); //0.01
System.out.println((a - b) == c); //false

整数转浮点精度损失


即使浮点数double范围比long大,但是转换浮点后还是会有精度损失
因为double内部的有效数字位53位,而long是64位
类似float内部的有效数字位24位,int是32位同样会出现精度损失
可以想象doublelong同为64位,但是存储方式截然不同,因此double为了追求更大的范围并不能无损覆盖long

Sample
1
2
3
4
5
6
7
8
9
10
long x = Long.MAX_VALUE;
double y = Long.MAX_VALUE;
long z = Long.MAX_VALUE - 1;
System.out.println(x); //9223372036854775807
System.out.println(y); //9.223372036854776E18
System.out.println(z); //9223372036854775806

System.out.println(x == y); //true 都转换浮点,同时损失
System.out.println(y == z); //true 都转换浮点,同时损失
System.out.println(x == z); //false

也就是浮点数足够大,造成精度损失后,变化1不足以让浮点改变
ulp函数可以检测浮点数间的差距

Sample
1
2
3
float a = 200000000f;
System.out.println(a == a + 1); //true
System.out.println(Math.ulp(a)); //16.0 至少16以上的变化才能让浮点改变

除0


普通的整数除以0,会报java.lang.ArithmeticException: / by zero
然而浮点数是可以除0的,Double类就定义了特殊的变量

Sample
1
2
public static final double POSITIVE_INFINITY = 1.0 / 0.0;
public static final double NEGATIVE_INFINITY = -1.0 / 0.0;

因为无穷有正负之分,因此0和-0也有点小区别

Sample
1
2
System.out.println(1 / 0d); //Infinity
System.out.println(1 / -0d); //-Infinity

0还可以除0

Sample
1
public static final double NaN = 0.0d / 0.0;

一切由NAN参与的运算结果都是NAN,涉及到NAN的判定都是false

数值比较


如果类型不匹配,可能发生意外
如果采用equals方法,会走装箱策略,变成两个对象比较,由于类型不同,直接不相等
如果采用==方法,会走拆箱策略变成数值比较

Sample
1
2
3
4
short num = 1;
Integer integer = 1;
System.out.println(integer.equals(num)); //false
System.out.println(integer == num); //true

还要注意拆箱过程中null影响

Sample
1
2
Integer a = null;
System.out.println(a == 1); //NullPointerException

使用Java7提供的对象比较处理

Sample
1
2
Integer a = null;
System.out.println(Objects.equals(a, 1)); //false

典型的场景是Map, 它是通过equal判定命中
如果定义了Map<Long,Object>,那么取值时一定要注意类型

Sample
1
2
3
Map<Long, Object> map = new HashMap<>();
map.put(1L, "str");
System.out.println(map.get(1)); //null

如果确定是同种数字比较的话,可以借助字符串比较

Sample
1
2
3
4
public static <T extends Number> boolean compareMix(T num1, T num2) {
return Objects.equals(num1, num2) || Objects.toString(num1).equals(Objects.toString(num2));
}
System.out.println(compareMix(new Integer(1), (short)1)); //true

特殊的最小值


整数范围并不是对称的,最小值Integer.MIN_VALUE-2147483648,而最大值Integer.MAX_VALUE2147483647,也就是负数的范围比正数要大

Integer.MIN_VALUE是个比较特殊的值,二进制表示是0x80000000,对这个数取负数0x7fffffff + 1依然是本身,实际上是溢出了。而java本身不考虑溢出,会产生如下诡异的结果

Sample
1
boolean res = Integer.MIN_VALUE == -Integer.MIN_VALUE; //true

0x7fffffff + 1可以看成最大再加1,即最大值增加后会回到最小值

Sample
1
boolean res = Integer.MAX_VALUE + 1 == Integer.MIN_VALUE; //true

因为正负范围不对等,因此if(num < 0) num = -num;是不对的,没有考虑到极限情况
绝对值函数遇到Integer.MIN_VALUE会原样返回,因此可能是负值

Sample
1
int abs = Math.abs(Integer.MIN_VALUE); //-2147483648

最小值再减1也会变成最大值

Sample
1
boolean res = Integer.MIN_VALUE - 1 == Integer.MAX_VALUE; //true

运算优先级


取余%和乘*具有相同的优先级

1
2
int ex1 = 2 % 10 * 10;
int ex2 = 2 % 100;

移位运算


移位是取余的,对于整数移位32位会被认为等同于移0位
因此Java中无法把一个数通过一次移位置0

Sample
1
2
3
4
5
6
7
int i = -1;
i = i >>> 31;
i = i >>> 1;
System.out.println(i); //0
i = -1;
i = i >>> 32;
System.out.println(i); //-1

取余运算


Java的取余运算支持负数。。结果符号和最左边操作数一致
绝大多数情况并不需要负数,应考虑排除

Sample
1
2
3
4
System.out.println(5 % 2);   //1
System.out.println(-5 % 2); //-1
System.out.println(5 % -2); //1
System.out.println(-5 % -2); //-1

三目运算


三目运算不完全等同于if-else,还有隐式的类型转换
如果比较的两者类型不等,并且其中包含装箱类型,那么会先拆箱成基本类型,然后转成较大的类型

Sample
1
2
3
4
5
6
Object a = true ? Integer.valueOf(1) : Double.valueOf(1.2); //1.0

Integer nullInt = null;
Object b = true ? nullInt : Integer.valueOf(1); //null
Object c = true ? nullInt : 1; //NullPointerException
Object d = true ? nullInt : Double.valueOf(1.2); //NullPointerException

三目运算最好避免不同类型混合

总结


  • 字面数字默认是int和double
  • 考虑拆装箱影响
  • 数字比较大考虑溢出
  • 避免不同类型数值混合计算