introduction
由一个空指针引发的思考
一.java值传递
定义:
值传递(
pass
by
value
)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 引用传递(pass
by
reference
)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
先说结论,java
是值传递,因为你无法在函数中改变原始对象,对象里面的内容不算。
例子:
1.很明显结果是0和2,set
尝试去改变形参的引用但是结果却什么都没改变到,set
2改变形参里面的值却是可以的。为什么不能改变原始对象的引用呢?我们用字节码分析一下。
栈帧定义
set 栈帧初始状态
0 new 创建一个对象 放在操作数栈
3 dup 复制操作数栈栈顶的值,并且入栈
4 将1push到操作数栈
此时栈帧的状态
5 执行 init方法
8 将栈顶存入局部变量表
可以看到,在8 将栈顶存入局部变量表的时候,改变的只是本地的局部变量表,所以也没有改变原始对象,也改变不了。
2.对于set
2,改变里面的值,分析如下。
子节码
0 将局部变量表1位置的对象放入栈顶
1 将2,放入栈顶
2 设值
3 返回
可以看到,设置值的时候调用了putfield
指令,改变了val的值。
二.赋值操作
代码:
public class ByteCode1 {
public void test2() {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 128;
Integer f = 128;
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));
}
}
读到这里,大家不妨思考一下,输出会是什么。涉及到自动装箱,对象的缓存,==和equals
的区别,自动拆箱:包装类的“==”运算在不遇到算术元算的情况下不会自动拆箱。
可以看到,对于Integer
a
= 1,字节码编译解释就是,调用了Integer.valueOf,代码如下:
想必已经清楚的看到了,对于[-128,127]对的值,Integer
自己缓存了一个对象池,每次会从里面取,只有不在这个范围内的才会新创建Integer
。
总结一下:
1.装箱是调用Integer
的Integer
.valueOf
() 方法,该方法对于[-128,127]的Integer
变量做了缓存,所以[-128,127]直接Integer
对象直接==对true
。反之为fasle
。
2.==是比较引用 ,equals
由各自实现,对于包装类比较值应该用equals,除非你真的想比较引用。
三.空指针BUG
场景:java
8,有位童鞋用了类似如下代码,导致测试的程序启动不了,报空指针错误,但是开发的电脑却可以启动,反编译之后代码也是一样的,所以不存在更新问题。
public class Test {
public static void main(String[] args) {
testt();
}
static void testt() {
Map<String, Integer> test = new HashMap<>();
test.put("-2", null);
Integer tt2 = (true ? test.get("-2") : 0);
}
}
报错的class和未报错的class文件反编译结果
一般的问题看到反编译结果基本上可以解决了,例如更新不及时,编译不及时,导致文件差异,但是这个NPE
问题却十分诡异,后面了解之后发现,Eclipse
已经实现了自己的编译器,命名为 Eclipse
编译器for
Java
(ECJ)。而我们的IDEA使用的是javac编译,导致编译结果不一样,下面我们从字节码方面分析一下。
左边是javac
编译的字节码,右边是ECJ
编译的字节码,可以看到字节码29中,ECJ
对null
对象进行了拆箱,然后在自动封箱,在拆箱的时候报了空指针错误,所以对于3目运算符,我们要慎用,尽量不要出现这种情况,可以这样改进代码来避免拆箱(当然也可以避免3目运算符)。
public class Test {
public static void main(String[] args) {
testt();
}
static void testt() {
Map<String, Integer> test = new HashMap<>();
test.put("-2", null);
Integer tt2 = (true ? test.get("-2") : Integer.valueOf(0));
}
}
所以结论就出来了,开发集体是用的IDEA
,测试集体用的Eclipse
,导致了结果不一致,改一下代码即可解决,不过这个坑比较深,比较难以发现。