一文浅析Java中的值传递

 更新时间:2023年08月01日 08:56:00   作者:HuskySir  
今天在解决一个问题时,程序总是不能输出正确值,分析逻辑思路没问题后,发现原来是由于函数传递导致了这个情况,下面我们就来看看Java中的值传递到底是什么情况吧

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]]

我的代码如下

class Solution {
    public void traversal(TreeNode root, int count, List<List<Integer>> res, List<Integer> path) {
        path.add(root.val);
        if (root.left == null && root.right == null) {
            if (count - root.val == 0) {
                res.add(path);
            }
            return;
        }
​
        if (root.left != null) {
            traversal(root.left, count - root.val, res, path);
            path.remove(path.size() - 1);
        }
        if (root.right != null) {
            traversal(root.right, count - root.val, res, path);
            path.remove(path.size() - 1);
        }
    }
​
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        if (root == null) return res;
        traversal(root, targetSum, res, path);
​
        return res;
    }
}

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

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

输出:[[5],[5]]

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

原代码部分内容为

if (root.left == null && root.right == null) {
    if (count - root.val == 0) {
        res.add(path);
    }
    return;
}

该部分内容需要改为

if (root.left == null && root.right == null) {
    if (count - root.val == 0) {
        res.add(new ArrayList(path));
    }
    return;
}

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

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

输出:[[5,4,11,2],[5,8,4,5]]

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

数据类型占用位数默认值包装类
byte(字节型)80Byte
short(短整型)160Short
int(整型)320Integer
long(长整型)640.0lLong
float(浮点型)320.0fFloat
double(双精度浮点型)640.0dDouble
char(字符型)16"/u0000"Character
boolean(布尔型)1falseBoolean

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

基本数据类型的值传递

测试类

public class TestClass {
    public static void test(int value) {
        value = 2;
        System.out.println("形参value的值:" + value);
    }
​
    public static void main(String[] args) {
        int value = 1;
        System.out.println("调用函数前value的值:" + value);
        test(value);
        System.out.println("调用函数后value的值:" + value);
    }
}

结果为

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

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

引用数据类型的值传递

类TreeNode

public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
​
    TreeNode() {
    }
​
    TreeNode(int val) {
        this.val = val;
    }
​
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

测试类1

public class TestClass {
    public static void test(TreeNode node) {
        node.val = 2;
        System.out.println("形参node的val值:" + node.val);
    }
​
    public static void main(String[] args) {
        TreeNode node = new TreeNode(1);
        System.out.println("调用函数前node的val值:" + node.val);
        test(node);
        System.out.println("调用函数后node的val值:" + node.val);
    }
}

结果为

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

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

现在看另一个示例

测试类2

public class TestClass {
    public static void test(TreeNode node) {
        node = new TreeNode(2);
        System.out.println("形参node的val值:" + node.val);
    }
​
    public static void main(String[] args) {
        TreeNode node = new TreeNode(1);
        System.out.println("调用函数前node的val值:" + node.val);
        test(node);
        System.out.println("调用函数后node的val值:" + node.val);
    }
}

结果为

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

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

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

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

以下代码可验证上述分析

测试类1

public class TestClass {
    public static void test(TreeNode node) {
        System.out.println("test:node" + node);
        node.val = 2;
        System.out.println("test:node" + node);
        System.out.println("形参node的val值:" + node.val);
    }
​
    public static void main(String[] args) {
        TreeNode node = new TreeNode(1);
        System.out.println("调用函数前node的val值:" + node.val);
        System.out.println("main node:" + node);
        test(node);
        System.out.println("调用函数后node的val值:" + node.val);
        System.out.println("main node:" + node);
    }
}

结果为

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

测试类2

public class TestClass {
    public static void test(TreeNode node) {
        System.out.println("test:node" + node);
        node = new TreeNode(2);
        System.out.println("test:node" + node);
        System.out.println("形参node的val值:" + node.val);
    }
​
    public static void main(String[] args) {
        TreeNode node = new TreeNode(1);
        System.out.println("调用函数前node的val值:" + node.val);
        System.out.println("main node:" + node);
        test(node);
        System.out.println("调用函数后node的val值:" + node.val);
        System.out.println("main node:" + node);
    }
}

结果为

调用函数前node的val值:1
main node:TreeNode@1540e19d
test:nodeTreeNode@1540e19d
test:nodeTreeNode@677327b6
形参node的val值:2
调用函数后node的val值:1
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

import java.util.ArrayList;
import java.util.List;
​
public class TestClass {
    public static void main(String[] args) {
        List<List<Integer>> listList = new ArrayList<>();
        List<Integer> list1 = new ArrayList<>();
        list1.add(1);
        listList.add(list1);  //直接add list1
        List<Integer> list2 = new ArrayList<>();
        list2.add(2);
        listList.add(list2);
        System.out.println("list1改变前");
        for (List<Integer> l : listList) {
            for (Integer i : l) {
                System.out.println(i);
            }
            System.out.println("---");
        }
        list1.set(0, 2);    //将list1的0号元素改为2
        System.out.println("list1改变后");
        for (List<Integer> l : listList) {
            for (Integer i : l) {
                System.out.println(i);
            }
            System.out.println("---");
        }
    }
}

结果为

list1改变前
1
---
2
---
list1改变后
2
---
2
---

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

import java.util.ArrayList;
import java.util.List;
​
public class TestClass {
    public static void main(String[] args) {
        List<List<Integer>> listList = new ArrayList<>();
        List<Integer> list1 = new ArrayList<>();
        list1.add(1);
        listList.add(new ArrayList<>(list1)); //构造新对象 再调用add
        List<Integer> list2 = new ArrayList<>();
        list2.add(2);
        listList.add(list2);
        System.out.println("list1改变前");
        for (List<Integer> l : listList) {
            for (Integer i : l) {
                System.out.println(i);
            }
            System.out.println("---");
        }
        list1.set(0, 2);    //将list1的0号元素改为2
        System.out.println("list1改变后");
        for (List<Integer> l : listList) {
            for (Integer i : l) {
                System.out.println(i);
            }
            System.out.println("---");
        }
    }
}

结果为

list1改变前
1
---
2
---
list1改变后
1
---
2
---

到此这篇关于一文浅析Java中的值传递的文章就介绍到这了,更多相关Java值传递内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java多线程之线程状态详解

    Java多线程之线程状态详解

    这篇文章主要介绍了Java多线程 线程状态原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2021-09-09
  • Java数据结构之HashMap源码深入分析

    Java数据结构之HashMap源码深入分析

    Java HashMap是一种基于哈希表实现的键值对存储结构,可以实现快速的数据查找和存储。它是线程不安全的,但在单线程环境中运行效率高,被广泛应用于Java开发中
    2023-04-04
  • java识别一篇文章中某单词出现个数的方法

    java识别一篇文章中某单词出现个数的方法

    这篇文章主要介绍了java识别一篇文章中某单词出现个数的方法,涉及java字符解析操作的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-10-10
  • Java中RedisUtils工具类的使用

    Java中RedisUtils工具类的使用

    本文主要介绍了Java中RedisUtils工具类的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • java.lang.String和java.util.NClob互相转换方式

    java.lang.String和java.util.NClob互相转换方式

    这篇文章主要介绍了java.lang.String和java.util.NClob互相转换方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • mybatis关系映射之一对多和多对一

    mybatis关系映射之一对多和多对一

    今天小编就为大家分享一篇关于mybatis关系映射之一对多和多对一,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • Java调取创蓝253短信验证码的实现代码

    Java调取创蓝253短信验证码的实现代码

    这篇文章主要介绍了Java调取创蓝253短信验证码的实现代码,需要的朋友可以参考下
    2018-04-04
  • springboot aspect通过@annotation进行拦截的实例代码详解

    springboot aspect通过@annotation进行拦截的实例代码详解

    这篇文章主要介绍了springboot aspect通过@annotation进行拦截的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-08-08
  • 使用SpringBoot根据配置注入接口的不同实现类(代码演示)

    使用SpringBoot根据配置注入接口的不同实现类(代码演示)

    使用springboot开发时经常用到@Autowired和@Resource进行依赖注入,但是当我们一个接口对应多个不同的实现类的时候如果不进行一下配置项目启动时就会报错,那么怎么根据不同的需求注入不同的类型呢,感兴趣的朋友一起看看吧
    2022-06-06
  • SpringBoot配置shiro安全框架的实现

    SpringBoot配置shiro安全框架的实现

    这篇文章主要介绍了SpringBoot配置shiro安全框架的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04

最新评论