数字运算那些事

发表于:,更新于:,By Guodong
大纲
  1. 1. 假设你遇到了下面的笔试题
  2. 2. 编译
  3. 3. 字节码
  4. 4. 总结

本文介绍一些java数字运算的问题,通过阅读,希望你能清晰的了解java的数字运算。

假设你遇到了下面的笔试题

假设你遇到了下面的笔试题,你会给出什么答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestInteger {
public static void main (String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a+b));
System.out.println(c.equals(a+b));
System.out.println(g == (a+b));
System.out.println(g.equals(a+b));
}
}

要得到正确结果,不了解java文件的编译和字节码技术,是不可能彻底明白的。

编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

public class TestInteger {
public TestInteger() {
}

public static void main(String[] args) {
Integer a = Integer.valueOf(1);
Integer b = Integer.valueOf(2);
Integer c = Integer.valueOf(3);
Integer d = Integer.valueOf(3);
Integer e = Integer.valueOf(321);
Integer f = Integer.valueOf(321);
Long g = Long.valueOf(3L);
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c.intValue() == a.intValue() + b.intValue());
System.out.println(c.equals(Integer.valueOf(a.intValue() + b.intValue())));
System.out.println(g.longValue() == (long)(a.intValue() + b.intValue()));
System.out.println(g.equals(Integer.valueOf(a.intValue() + b.intValue())));
}
}

很容易拿到class文件的反编译结果。
可以看到,定义的所有变量 都被转化为封装类。

下面两条语句没有发生变化:
System.out.println(c == d);
System.out.println(e == f);

从下面这条语句可以看出,在进行数字的基本运算时,数字被还原为基本类型。
System.out.println(c.intValue() == a.intValue() + b.intValue());

从下面这条语句可以看出,基本类型作为equals参数时,会自动转换为包装类。
System.out.println(c.equals(Integer.valueOf(a.intValue() + b.intValue())));

从下面这条语句可以看出,当==两边的数字类型不一致时,占有字节较小的一侧会发生类型强转。
System.out.println(g.longValue() == (long)(a.intValue() + b.intValue()));

Ok,原来不太明白的话,读到这里你应该能给出这样的运行结果了吧?
true
false
true
true
true
false

大致说明一下为什么是这样的结果。
第1,2个输出的原因:
看一下Integer类的源码中这段代码你就明白了。

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

第3,4,5,你可能认为理应如此。
第6个输出为false,可以看下java.lang.Long#equals

如果你觉得你已经彻底明白了,你就错了,输出结果是这样的,还有一部分原因是class文件中的字节码技术。

字节码

下面两条语句,虽然结果都是ture,但是,他们生成字节码中进行比较的指令也是不同的。
System.out.println(c == d);
System.out.println(3 == 3);

我们把之前class文件的字节码拿出来几段分析一下。
下面的字节码应以了一个常量,调用了Method java/lang/Integer.valueOf获取常量的值。

1
2
3
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1

下面的字节码取出c和d两个变量,使用if_acmpne指令进行对比,这个命令比较的是两个对象的引用地址。

1
2
3
48: aload_3
49: aload 4
51: if_acmpne 58

下面的字节码取出c,a,b三个变量,a和b使用iadd命令相加,然后使用了if_icmpne命令比较两个变量的数值。

1
2
3
4
5
6
7
8
83: aload_3
84: invokevirtual #8 // Method java/lang/Integer.intValue:()I
87: aload_1
88: invokevirtual #8 // Method java/lang/Integer.intValue:()I
91: aload_2
92: invokevirtual #8 // Method java/lang/Integer.intValue:()I
95: iadd
96: if_icmpne 103

总结

如果想彻底了解java的数字运算,必须要知道编译后生成的class文件,甚至去查看字节码。
如果不想知其所以然,也可以通过下面的方式记忆。

  1. 如果你定义的是基本类型,编译器会根据你定义数字的具体大小,来描述你的变量。
    比如你定义了int a = 1;但是在程序中没有使用,那编译后生成的字节码中可能是如下的结果:
    boolean a = true;
    如果你使用了,可能又是下面的结果:
    byte h = 1;
  2. 如果你定义的是包装类,而且是把基本类型赋值给包装类,那编译器会调用包装类的valueOf方法。
  3. 封装类进行基本运算都会转化成基本类型。
  4. ==两端,如果有一侧进行了数字运算,那么它使用if_icmpne指令来比较是否相等,也就是说比较数值大小。
  5. ==两端,如果两侧都没有进行数字运算,那么它使用if_acmpne指令来比较是否相等,也就是说比较的是引用地址。

大概就是这样了,开发jdk的大牛们,为了jvm更高效,让我们现在会遇到各种各样奇怪的问题。
所以,我们在比较数字是否相等时,最好这样做:

  1. 将数字转化为基本类型,使用==进行比较。
  2. 将数字都转化成同样类型的包装类,使用equals进行比较。

PS:上面的面试题其实意义不大,会的人也可能在工作中写出很差的代码,不会的人也不一定写不出好的代码。大部分面试官看不出一个人的能力,只能量化标准。正所谓千里马常有,伯乐不常有。