注意!JAVA中的值传递

注意,java,传递 · 浏览次数 : 221

小编点评

测试代码主要用于测试构造函数、新对象赋值操作、原对象赋值操作及排版操作。 主要测试内容包括: 1. 创建一个ArrayList,并将其存储在listList中。 2. 使用for循环遍历listList,并打印每个元素的值。 3. 创建一个新的ArrayList,并将其存储在listList中。 4. 使用for循环遍历listList,并添加元素2到listList中。 5. 将list1的0号元素改为221。 6. 使用for循环遍历listList,并打印每个元素的值。 7. 设置list1的0号元素的值为221。 8. 使用for循环遍历listList,并打印每个元素的值。 测试结果显示,list1改变前和后的值分别为2 1 3 4 2 5 6 7 8 210,说明原对象的值发生改变。 测试代码主要使用以下方法: * ArrayList的add()方法用于添加元素到ArrayList中。 * ArrayList的set()方法用于设置元素的值。 * ArrayList的clear()方法用于清除ArrayList中的所有元素。 * for循环遍历List,并打印每个元素的值。 测试代码主要利用了ArrayList的add()方法、set()方法、clear()方法和for循环等方法,来实现原对象的值发生改变。

正文

前言:今天在解决一个问题时,程序总是不能输出正确值,分析逻辑思路没问题后,发现原来是由于函数传递导致了这个情况。

LeetCode 113

问题:给你二叉树的根节点root和一个整数目标和targetSum,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

示例

 

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

我的代码如下

 1 class Solution {
 2     public void traversal(TreeNode root, int count, List<List<Integer>> res, List<Integer> path) {
 3         path.add(root.val);
 4         if (root.left == null && root.right == null) {
 5             if (count - root.val == 0) {
 6                 res.add(path);
 7             }
 8             return;
 9         }
10 11         if (root.left != null) {
12             traversal(root.left, count - root.val, res, path);
13             path.remove(path.size() - 1);
14         }
15         if (root.right != null) {
16             traversal(root.right, count - root.val, res, path);
17             path.remove(path.size() - 1);
18         }
19     }
20 21     public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
22         List<List<Integer>> res = new ArrayList<>();
23         List<Integer> path = new ArrayList<>();
24         if (root == null) return res;
25         traversal(root, targetSum, res, path);
26 27         return res;
28     }
29 }

该题的思路是采用递归,traversal函数内root是当前树的根节点,count是目标值,res是存储结果,path是路径。该代码对于示例的输入输出为

1 输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
2 输出:[[5],[5]]

经过排查最终问题在于代码中的add方法

原代码部分内容为

1 if (root.left == null && root.right == null) {
2     if (count - root.val == 0) {
3         res.add(path);
4     }
5     return;
6 }

该部分内容需要改为

1 if (root.left == null && root.right == null) {
2     if (count - root.val == 0) {
3         res.add(new ArrayList(path));
4     }
5     return;
6 }

此时所有代码对于示例的输入输出为

1 输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
2 输出:[[5,4,11,2],[5,8,4,5]]

在java中,存在8大基本数据类型,且均有对应的包装类

数据类型占用位数默认值包装类
byte(字节型) 8 0 Byte
short(短整型) 16 0 Short
int(整型) 32 0 Integer
long(长整型) 64 0.0l Long
float(浮点型) 32 0.0f Float
double(双精度浮点型) 64 0.0d Double
char(字符型) 16 '/u0000' Character
boolean(布尔型) 1 false Boolean

在java中,函数传递只有值传递,是指在调用函数时,将实际参数复制一份传递给函数,这样在函数中修改参数(形参)时,不会影响到实际参数。

基本数据类型的值传递

测试类

 1 public class TestClass {
 2     public static void test(int value) {
 3         value = 2;
 4         System.out.println("形参value的值:" + value);
 5     }
 6  7     public static void main(String[] args) {
 8         int value = 1;
 9         System.out.println("调用函数前value的值:" + value);
10         test(value);
11         System.out.println("调用函数后value的值:" + value);
12     }
13 }

结果为

1 调用函数前value的值:1
2 形参value的值:2
3 调用函数后value的值:1

结论:可以看到,int类型的value初始为1,调用函数后,value仍然为1,基本数据类型在函数中修改参数(形参)时不会影响到实参的值。

引用数据类型的值传递

类TreeNode

 1 public class TreeNode {
 2     int val;
 3     TreeNode left;
 4     TreeNode right;
 5  6     TreeNode() {
 7     }
 8  9     TreeNode(int val) {
10         this.val = val;
11     }
12 13     TreeNode(int val, TreeNode left, TreeNode right) {
14         this.val = val;
15         this.left = left;
16         this.right = right;
17     }
18 }

测试类1

 1 public class TestClass {
 2     public static void test(TreeNode node) {
 3         node.val = 2;
 4         System.out.println("形参node的val值:" + node.val);
 5     }
 6  7     public static void main(String[] args) {
 8         TreeNode node = new TreeNode(1);
 9         System.out.println("调用函数前node的val值:" + node.val);
10         test(node);
11         System.out.println("调用函数后node的val值:" + node.val);
12     }
13 }

结果为

1 调用函数前node的val值:1
2 形参node的val值:2
3 调用函数后node的val值:2

结论:可以看到,TreeNode类型的node对象的val值初始为1,调用函数后,node对象的val值被修改为2,引用数据类型在函数中修改参数(形参)时影响到了实参的值。

现在看另一个示例

测试类2

 1 public class TestClass {
 2     public static void test(TreeNode node) {
 3         node = new TreeNode(2);
 4         System.out.println("形参node的val值:" + node.val);
 5     }
 6  7     public static void main(String[] args) {
 8         TreeNode node = new TreeNode(1);
 9         System.out.println("调用函数前node的val值:" + node.val);
10         test(node);
11         System.out.println("调用函数后node的val值:" + node.val);
12     }
13 }

结果为

1 调用函数前node的val值:1
2 形参node的val值:2
3 调用函数后node的val值:1

结论:可以看到,TreeNode类型的node对象的val值初始为1,调用函数后,node对象的val值仍然为1,引用数据类型在函数中修改参数(形参)时未影响到实参的值。

那么,为什么会出现这种问题呢?

首先,在JAVA中,函数传递都是采用值传递,实际参数都会被复制一份给到函数的形式参数,所以形式参数的变化不会影响到实际参数,基本数据类型的值传递示例可以发现这个性质。但引用数据类型的值传递为什么会出现修改形式参数的值有时会影响到实际参数,而有时又不会影响到实际参数呢?其实引用数据类型传递的内容也会被复制一份给到函数的形式参数,这个内容类似C++中的地址,示例中的node对象存储于堆中,虽然形参与实参是两份内容,但内容值相同,都指向堆中相同的对象,故测试类1在函数内修改对象值时,函数外查看时会发现对象值已被修改。测试类2在函数内重新构造了一个对象node,在堆中申请了一个新对象(新对象与原对象val值不相同),让形参指向这个对象,所以不会影响到原对象node的值。测试类1与测试类2的区别在于引用数据类型的指向对象发生了变化。

以下代码可验证上述分析

测试类1

 1 public class TestClass {
 2     public static void test(TreeNode node) {
 3         System.out.println("test:node" + node);
 4         node.val = 2;
 5         System.out.println("test:node" + node);
 6         System.out.println("形参node的val值:" + node.val);
 7     }
 8  9     public static void main(String[] args) {
10         TreeNode node = new TreeNode(1);
11         System.out.println("调用函数前node的val值:" + node.val);
12         System.out.println("main node:" + node);
13         test(node);
14         System.out.println("调用函数后node的val值:" + node.val);
15         System.out.println("main node:" + node);
16     }
17 }

结果为

1 调用函数前node的val值:1
2 main node:TreeNode@1540e19d
3 test:nodeTreeNode@1540e19d
4 test:nodeTreeNode@1540e19d
5 形参node的val值:2
6 调用函数后node的val值:2
7 main node:TreeNode@1540e19d

测试类2

 1 public class TestClass {
 2     public static void test(TreeNode node) {
 3         System.out.println("test:node" + node);
 4         node = new TreeNode(2);
 5         System.out.println("test:node" + node);
 6         System.out.println("形参node的val值:" + node.val);
 7     }
 8  9     public static void main(String[] args) {
10         TreeNode node = new TreeNode(1);
11         System.out.println("调用函数前node的val值:" + node.val);
12         System.out.println("main node:" + node);
13         test(node);
14         System.out.println("调用函数后node的val值:" + node.val);
15         System.out.println("main node:" + node);
16     }
17 }

结果为

1 调用函数前node的val值:1
2 main node:TreeNode@1540e19d
3 test:nodeTreeNode@1540e19d
4 test:nodeTreeNode@677327b6
5 形参node的val值:2
6 调用函数后node的val值:1
7 main node:TreeNode@1540e19d

对于测试类1,形参和实参都是指向相同的对象,所以利用形参修改对象的值,实参指向的对象的值发生改变。对于测试类2,形参在函数开始和实参指向相同的对象,让其指向新的对象后,实参指向的对象的值不会发生改变。简要说,测试类1形参复制了实参的地址,修改了地址对应的对象值,但并未修改地址值,测试类2形参复制了实参的地址,并修改了地址值,但并未修改原地址值对应的对象值。


有了目前的结论,可以理解为什么res.add()函数内path修改为new ArrayList(path)就可代码运行成功。因为我的path类型为List<Integer>,为引用数据类型,且path的值一直在发生变化。随着递归代码的运行,path的值发生变化,res内最初的List<Integer>值会发生变化(就是path的值)。但将path修改为new ArrayList(path)后,是在堆中新构造了对象,并指向该对象,原对象的变化不会影响到该对象的值,那么res内List<Integer>值就不会发生变化。

listList.add()方法直接传入list1

 1 import java.util.ArrayList;
 2 import java.util.List;
 3  4 public class TestClass {
 5     public static void main(String[] args) {
 6         List<List<Integer>> listList = new ArrayList<>();
 7         List<Integer> list1 = new ArrayList<>();
 8         list1.add(1);
 9         listList.add(list1);  //直接add list1
10         List<Integer> list2 = new ArrayList<>();
11         list2.add(2);
12         listList.add(list2);
13         System.out.println("list1改变前");
14         for (List<Integer> l : listList) {
15             for (Integer i : l) {
16                 System.out.println(i);
17             }
18             System.out.println("---");
19         }
20         list1.set(0, 2);    //将list1的0号元素改为2
21         System.out.println("list1改变后");
22         for (List<Integer> l : listList) {
23             for (Integer i : l) {
24                 System.out.println(i);
25             }
26             System.out.println("---");
27         }
28     }
29 }

结果为

 1 list1改变前
 2 1
 3 ---
 4 2
 5 ---
 6 list1改变后
 7 2
 8 ---
 9 2
10 ---

listList.add()方法重新构造新对象(内容与list1相同)

 1 import java.util.ArrayList;
 2 import java.util.List;
 3  4 public class TestClass {
 5     public static void main(String[] args) {
 6         List<List<Integer>> listList = new ArrayList<>();
 7         List<Integer> list1 = new ArrayList<>();
 8         list1.add(1);
 9         listList.add(new ArrayList<>(list1)); //构造新对象 再调用add
10         List<Integer> list2 = new ArrayList<>();
11         list2.add(2);
12         listList.add(list2);
13         System.out.println("list1改变前");
14         for (List<Integer> l : listList) {
15             for (Integer i : l) {
16                 System.out.println(i);
17             }
18             System.out.println("---");
19         }
20         list1.set(0, 2);    //将list1的0号元素改为2
21         System.out.println("list1改变后");
22         for (List<Integer> l : listList) {
23             for (Integer i : l) {
24                 System.out.println(i);
25             }
26             System.out.println("---");
27         }
28     }
29 }

结果为

 1 list1改变前
 2 1
 3 ---
 4 2
 5 ---
 6 list1改变后
 7 1
 8 ---
 9 2
10 ---

 

结论:调用构造函数后,函数指向新的对象,原对象的值发生改变,函数内值也不会改变。同理,新对象的值发生改变,原对象的值也不会发生改变。

与注意!JAVA中的值传递相似的内容:

注意!JAVA中的值传递

Java值传递学习总结

在线问诊 Python、FastAPI、Neo4j — 创建药品节点

目录前提条件创建节点 Demo准备数据创建药品标签节点 在线问诊 Python、FastAPI、Neo4j — 创建节点 Neo4j 节点的标签可以理解为 Java 中的实体。 根据常规流程:首先有什么症状,做哪些对应的检查,根据检查诊断什么疾病,需要用什么药物治疗,服药期间要注意哪些饮食,需要做哪

关于对于Java中Entity以及VO,以及DTO中Request对象序列化的学习

关于 Serializable的探讨 前提引入 是由于软件测试上有同学提到说,什么该字段在程序刚运行时,导致jvm激增,所以吸引了我的注意 回顾代码 MybatisPlus Generator自动生成的entity中就经常带有这个, 而且我在开发代码的时候VO,以及DTO常常是直接复制对应的enti

[转帖]JVM 系列 - 内存区域

一、对象在JVM中的表示: OOP-Klass模型 https://www.jianshu.com/p/424a920771a3 写的很赞。 注意:OOP-Klass是hotspot的JVM实现原理,其他JVM的实现可能不一样。、 OOP表示java实例,Klass表示class。 Klass: 包

Java面试题:SimpleDateFormat是线程安全的吗?使用时应该注意什么?

在Java开发中,我们经常需要获取和处理时间,这需要使用到各种不同的方法。其中,使用SimpleDateFormat类来格式化时间是一种常见的方法。虽然这个类看上去功能比较简单,但是如果使用不当,也可能会引发一些问题。

JAVA多线程并发编程-避坑指南

本篇旨在基于编码规范、工作中积累的研发经验等,整理在多线程开发的过程中需要注意的部分,比如不考虑线程池参数、线程安全、死锁等问题,将会存在潜在极大的风险。并且对其进行根因分析,避免每天踩一坑,坑坑不一样。

Java面试题:Spring Bean线程安全?别担心,只要你不写并发代码就好了!

Spring Bean是单例模式,即在整个应用程序上下文中只有一个实例。在多线程环境下,Singleton Scope Bean可能会发生线程安全问题。Spring Bean是否线程安全取决于Bean的作用域和Bean本身的实现。在使用Singleton Scope Bean时需要特别注意线程安全问...

[转帖]java -d 参数(系统属性) 和 环境变量

https://www.cnblogs.com/limeiyang/p/16565920.html 1. -d 参数说明 通过 java -h 查看可知: 注意:-D= : set a system property 设置系统属性。如果value是一个包含空格的字符串,则必须将该字符串括在双引号中。

线程池使用小结

转载请注明出处: 在Java中,Executors是一个线程池的工厂类,它可以创建不同类型的线程池。下面是几种常见的Executors线程池,以及它们的使用区别: FixedThreadPool:这种类型的线程池有一个固定的线程数量,一旦线程池中的全部线程都在处理任务,那么后续提交的任务将会等待。如

SPI在Java中的实现与应用 | 京东物流技术团队

1 SPI的概念 API API在我们日常开发工作中是比较直观可以看到的,比如在 Spring 项目中,我们通常习惯在写 service 层代码前,添加一个接口层,对于 service 的调用一般也都是基于接口操作,通过依赖注入,可以使用接口实现类的实例。 简单形容就是这样的: 图1:API 如上图