Java传值调用和传引用调用方式(参数引用为null问题)
作者:Jerry的技术博客
一、问题
近期在项目中遇到一个场景,在多层级调用中需要传递上下文,调用过程中上线文对象可能为空,想通过一个公共方法处理上下文,当上下文为空时生成上下文对象,执行相关操作后将该上下文对象向后传递。
大致逻辑如下:
public class Test { public static void handleContext(Context context) { if(context == null) { context = new Context(); } context.addNum(); } public static void main(String[] args) { Context context = null; handleContext(context); System.out.println(context.getNum()); } static class Context { private int num; public int getNum() { return num; } public void addNum() { this.num ++; } } }
测试执行报空指针错误,context没有按设想在handleContext方法中生成对象。
原因是main方法在栈中创建了Context的引用并将其指向null,handleContext方法参数中的Context引用也被指向null,handleContext方法体在堆中创建Context对象,并将对象地址赋给方法参数中的Context引用,但main方法中的Context引用仍然为null,因此调用context.getNum()时报空指针。
二、java方法传值与传引用
2.1 变量存储
基础类型变量:
Java的8中基础数据类型:byte(8位)、short(16位)、int(32位)、long(64位)、float(32位)、double(64位)、char(16位)、boolean(8位),
基础类型的数据存储在栈中,即是栈中分配内存空间存储所包含的值,其值就代表数据本身,值类型的数据具有较快的存取速度。
引用类型变量:
除了基础类数据外,其余都是引用类型,包括类、数组等。
引用类型数据的具体对象存放在堆中,而栈中存放的是该对象的内存地址。
当引用类型没有赋值时,其引用为null,表示不指向任何对象。
2.2 创建对象与赋值过程
Context context = new Context();
该操作可以分为两个部分:
Context context; context = new Context();
第一步:创建Context引用,在栈中开辟一块空间用于存储该引用;
第二步:创建Context对象,在堆内存中开辟一块空间存储该对象,并将该对象的存储地址赋值给栈中的引用
2.3 引用赋值过程
将一个引用赋值给另一个引用时,其实是将该引用指向的地址赋值给另个应用,让两个引用指向同一个对象,示意图如下。
Context context1 = new Context(); Context context2 = context1;
2.4 传值与传引用方法调用
其实无论是传值还是传引用调用,其本质都是将栈中存储的数据复制一份,传递给栈中的另一个变量。
对于基础数据类型,是将数据本身复制一份传递;对于引用类型,是将引用的地址复制一份传递。
因此,上述问题可以描述为如下示意图。当handleContext方法执行返回后,main方法中的context引用仍然为null。
注:为方便理解,将null特殊处理。当引用为null时候表示不指向任何对象
值得注意的是包装类型(Intger;Long;Short;Double;Float;Char;Boolean;Byte,以及String(char[]的包装类型)),虽然是引用类型数据,但其效果等同于传值调用,示例如下:
public class Test { public static void change(String str) { str = "xyz"; } public static void main(String[] args) { String str = "abc"; change(str); System.out.println(str); } } --- abc
其原因是String类型是不可变(immutable)的。
String类型及其成员变量均是final的,这意味着String的value字符数组不能指向其它地址,同时value字符数组的值也不可能通过继承String后修改。
在change方法中,参数String引用str初始化时指向对象abc,执行方法后指向了方法区的另一个字符串常量(xyz),而main方法中的String引用str仍然指向方法区的字符串常量(abc)。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { ... private final char value[]; ... }
2.5 Java常量池
基本类型的包装类型大部分都实现了常量池技术,包括:Byte、Short、Integer、Long、Character、Boolean,但只有在其值 -128<value<127范围内才可使用常量池,数据存放在方法区,数值超出范围或通过new方法生成对象则会在堆上分配存储空间
public class Test { public static void main(String[] args) { Integer i = 123; Integer j = 123; System.out.println("i == j : " + (i == j)); System.out.println("i.equals(j) : " + i.equals(j)); Integer m = 1234; Integer n = 1234; System.out.println("m == n : " + (m == n)); System.out.println("m.equals(n) : " + m.equals(n)); Integer x = new Integer(123); Integer y = new Integer(123); System.out.println("x == y : " + (x == y)); System.out.println("x.equals(y) : " + x.equals(y)); } } --- i == j : true i.equals(j) : true m == n : false m.equals(n) : true x == y : false x.equals(y) : true
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。