再论 “Java 是值传递还是地址传递?”

前言

体能状态先于精神状态,习惯先于决心,聚焦先于喜好.

先说说上次测试的标准、结论及纠错

关于 Java 是值传递还是地址传递的问题在很早之前就遇到过,当时自己还做了一个实验,
当时的标准是,将一个局部变量传递到一个void 类型的方法种进行赋值,之后在方法外打印参数,如果参数值未修改,说明该类型是值传递,否则就是地址传递
当然经过本次探究,我上一次的标准是错误的,因为我把Java的引用传递和地址传递混为一谈了,修正后的结论(详情)
Java 8种基本类型(boolean、char、byte、short、int、long、float、double)以及对应的包装类型是值传递、String是值传递、Date类型是值传递
剩下的POJO 、List、Map、Set ,还有数组是引用传递,而本质上依旧是值传递

为什么从新讨论这个问题?

最近看《重构》,在小标题 **移除对参数的赋值(Remove Assignments to Parameters)**中,Martin Fowler (作者,这样感觉我们很熟一样有没有)强调Java 使用按值传递的函数调用方式,着常常造成许多人迷惑,在所有地点,Java 都严格采用值传递方式,这使我不禁窃喜,终于看到权威的论断了,可惜的是书中提供的例子是 Date 类型,然后这个并无法推翻我之前的逻辑,所以一定是哪里出了问题.

刨根问底:值传递的本质和地址传递的本质

值传递的本质是被调用者的拷贝传递给调用者,一般是实参传递给形参,形参修改不影响实参数,除非实参从新被形参赋值
地址传递的本质是将被调用者将自身的地址传递给调用者,并且允许调用者修改自身的内容
乍一看,Java 的POJO对象就是地址传递是吧?哈哈哈,且看下面的例子

思考两个问题,认识 Java 的引用传递

上面一条说到,地址传递允许被调用者修改自己在内存中的内容,接着思考下面两个问题

=null 后 Java Heap 中的对象消失了吗?

下面的第三步,list=null 会立即将 Java Heap 中 list 指向的对象空间清空吗?如果你了解 JVM 垃圾回收机制,这一步并不会导致空间被立即收回,尽管会促使 JVM 进行垃圾回收,因为list 只是指向了内存的一个地址,而不能说就是内存的内容.

List<String> list =new List<String>();
list.add("1");
list=null;
以 Map 区分Java 是引用传递而非地址传递

下面的例子中,输出结果是 jecket,如果Java Map 地址传递,那么最终输出结果应该是”hello world“,但是最终的结果表明,testMap 只是表示内存中的引用,事实上main中的map也只是内存地址的引用,及Java 是引用传递

public static void main(String[] args) {
		Map<String,String> map=new HashMap<String,String>();
		testMap(map);
		System.out.println(map.get("name"));//输出 jacket
	}
	
	public static void testMap(Map<String,String> map) {
		map.put("name", "jacket");
		map=new HashMap<String,String>();
		map.put("name", "hello world");
	}
对象在方法内部实例化后会受影响吗?

答案是:不会
在方法内部重新实力化是无法影响方法外的对象的,
这样只是将方法内部的局部变量指向了一个新地址,再次证明Java 不是地址传递,这里是引用传递

@Test
	public void test5() {
		List<String> list=new ArrayList<String>();
		List<String> list2=new ArrayList<String>();
		list.add(0, "100");
		list2.add(0, "100");
		System.out.println("外围1:"+(list==null)+";list:"+list.get(0)+";list2:"+list2);
		initList(list);
		changeList(list2);
		System.out.println("外围2:"+(list==null)+";list:"+list.get(0)+";list2:"+list2);
	}
	/**
	 * 内部从新 实例化,无法修改外面的参数
	 * */
	public void initList(List<String> l) {
		System.out.println("内部1:"+(l==null)+l.get(0));
		l=new ArrayList<String>();
		l.add(0,"200");
		System.out.println("内部2:"+(l==null)+l.get(0));
	}
	/**
	 * 内部仅仅新增一个 参数
	 * */
	public void changeList(List<String> l) {
		System.out.println("内部3:"+(l==null)+l.get(0));
		l.add(0,"200");
		System.out.println("内部4:"+(l==null)+l.get(0));
	}
外围1:false;list:100;list2:[100]
内部1:false100
内部2:false200
内部3:false100
内部4:false200
外围2:false;list:100;list2:[200, 100]
for 循环中的实例化

比如我们使用一个循环创建新对象,然后放入List中,下面的代码也是可以的,因为new会将声明的对象指向新分配的地址,而不是将其原地址改为另一个值,但是笔者并不建议这个做,从封装的角度来看,应该尽可能小的暴露

public static void main(String[] args) {
		Person p=null;
		List<Person> list=new ArrayList<Person>();
		
		for(int i=0;i<2;i++) {
		//建议在内部声明 Person p=new Person();
		//=并不会影响原内存地址内容,而是指向新的对象地址
			p=new Person();
			p.setAge(i);
			list.add(p);
		}
		
		System.out.println(list.get(0).getAge());//0
		System.out.println(list.get(1).getAge());//1
	}
最佳实践

尽管证明了Java 是值传递,但是Java 也存在本质是值传递的引用传递,请牢记
Java 8种基本类型(boolean、char、byte、short、int、long、float、double)以及对应的包装类型是值传递、String是值传递、Date类型是值传递
剩下的POJO 、List、Map、Set ,还有数组是引用传递

不要通过形参方式试图实例化一个方法外变量

鉴于你无法通过形参真正的操作一个对象的真实地址
所以,你不应该通过形参方法进行实例化工作(new一个对象),或者 =null
如果你想提供一个类似的方法,要么直接操作全局变量,要么提供返回值,即 return
思考一个工厂方法,产生一个对象,然后返回这个新建对象
当然,如果只是修改对象的属性值,这个是可以影响方法外的对象的.总之,不要认为你可以通过形参修改方法外对象在内存中的地址

List<String> list;
	
/**
 * 通过形参无法对方法外参数进行实力化-
 * @param list
 */
public void init(List list) {
	list=new ArrayList<String>();
	list.add("100");
}

@Test
public void test5() {
	System.out.println((list==null)?null:list.get(0));
	init(list);
	System.out.println((list==null)?null:list.get(0));
}
POJO 、List、Map、Set ,还有数组 引用传递在复杂场景中应用
  • 第一种:对象=另一个对象
    A=new1
    B1=A,B2=B1, 之后
    A=new2 或者 A=C
    这种 B1、B2还是指向new1的内存地址,而A会指向new2或者C的内存地址。就是说,整体赋值的时候,不会影响原先的依赖对象的值。
  • 第二种,对象.属性=另一个对象
    这种情况下,A=B,B.C=new 时,A.C也是会同时感知到的,这个是因为,修改对象的属性是修改的同一个地址的对象

在这里插入图片描述

对于第一种,特别要注意整体赋值时
A.C=new 不会改变A的内存地址指向
但是 A=A.C 也是整体赋值的情况,在处理链表场景时,特别注意,B=A,A=A.C 时,B保持了A的旧值,A变为了自己的子节点
也就是说, B=A,B.next=新值,会一起修改A和B的内存地址信息,然后B=B.next 会导致B指向新的内容地址,也就是A的叶子节点,然后B.next=新值,B=B.next ,此时A起始地址保持不变,而B已经指向A的叶子的叶子

在这里插入图片描述

参考文档

[1]、https://blog.csdn.net/qq_43171869/article/details/83349282
[2]、https://www.jianshu.com/p/b4d0c87273b6

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值