It's our wits that make us men.

由一个空指针引发的思考

Posted on By 刘电波

introduction

由一个空指针引发的思考

一.java值传递

定义:

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

先说结论,java是值传递,因为你无法在函数中改变原始对象,对象里面的内容不算。

例子:

1.很明显结果是0和2,set尝试去改变形参的引用但是结果却什么都没改变到,set2改变形参里面的值却是可以的。为什么不能改变原始对象的引用呢?我们用字节码分析一下。

栈帧定义

set 栈帧初始状态

0 new 创建一个对象 放在操作数栈
3 dup  复制操作数栈栈顶的值,并且入栈
4 将1push到操作数栈

此时栈帧的状态

5 执行 init方法

8 将栈顶存入局部变量表

可以看到,在8 将栈顶存入局部变量表的时候,改变的只是本地的局部变量表,所以也没有改变原始对象,也改变不了。

2.对于set2,改变里面的值,分析如下。

子节码

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.装箱是调用IntegerInteger.valueOf() 方法,该方法对于[-128,127]的Integer变量做了缓存,所以[-128,127]直接Integer对象直接==对true。反之为fasle

2.==是比较引用 ,equals由各自实现,对于包装类比较值应该用equals,除非你真的想比较引用。

三.空指针BUG

场景:java8,有位童鞋用了类似如下代码,导致测试的程序启动不了,报空指针错误,但是开发的电脑却可以启动,反编译之后代码也是一样的,所以不存在更新问题。

 
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中,ECJnull对象进行了拆箱,然后在自动封箱,在拆箱的时候报了空指针错误,所以对于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,导致了结果不一致,改一下代码即可解决,不过这个坑比较深,比较难以发现。