← 首页·文章列表

Java 值传递

一、问题引入:一段 “语法正确但逻辑失效” 的代码

1. 问题场景

实现 “查找二叉搜索树(BST)中第 k 小元素” 功能,以下代码编译无报错,但运行结果始终错误(返回 0):

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}

class Solution {
    public int kthSmallest(TreeNode root, int k) {
        int res = 0; // 外层局部变量
        dfs(root, k, res); // 传参调用递归方法
        return res; // 永远返回0,核心逻辑失效
    }

    // 中序遍历(左→根→右)找第k小元素
    private void dfs(TreeNode node, int k, int res) {
        if (node == null) return;
        dfs(node.left, k, res); // 递归左子树
        if (k == 0) return; // 提前终止(已找到目标)
        if (--k == 0) res = node.val; // 看似修改res,实际无效
        dfs(node.right, k, res); // 递归右子树
    }
}

2. 问题现象

无论输入的 BST 结构和 k 值如何,kthSmallest方法最终返回res的初始值 0,无法正确记录第 k 小元素的数值。

二、核心概念解析

1. Java 的 “值传递”(唯一传递方式)

(1)官方定义

Java 语言规范明确:所有参数传递都是值传递—— 方法调用时,会创建实参的 “副本” 并传递给形参,方法内仅能修改副本,无法直接修改原始实参本身。

(2)易混淆点澄清

❌ 错误认知:Java 有 “值传递” 和 “引用传递” 两种方式;

✅ 正确认知:引用类型变量传参时,传递的是 “引用地址的副本”,而非 “引用本身”,本质仍是值传递。

(3)不同类型的 “值传递” 细节

变量类型 实参存储的内容 传递的副本内容 方法内修改的范围 外层实参的变化
基本类型(int/char 等) 具体数值(如 3、0) 数值副本(如 3、0) 仅能修改副本的数值 无变化(原值不变)
引用类型(数组 / 对象) 内存地址(如 0x123) 地址副本(如 0x123) ① 可修改地址指向的对象 / 数组内容;

② 可修改形参的地址副本(指向新对象)
① 内容变化可感知;

② 地址本身无变化

(4)代码验证:引用类型的 “值传递” 本质

public class PassTest {
    // 尝试修改引用类型参数
    public static void modifyArr(int[] arr) {
        // ① 修改数组内容(通过地址副本找到原数组)
        arr[0] = 100;
        // ② 修改形参的地址副本(指向新数组)
        arr = new int[]{200, 300};
    }

    public static void main(String[] args) {
        int[] myArr = new int[]{1, 2}; // 实参指向地址0x123的数组
        modifyArr(myArr); // 传递地址0x123的副本
        
        // 输出:100 2(内容变,地址不变)
        System.out.println(myArr[0] + " " + myArr[1]);
    }
}

关键结论:若 Java 支持 “引用传递”,myArr应指向新数组{200,300},但实际仅数组内容被修改,地址未变 —— 证明本质是值传递。

2. 局部变量 vs 成员变量(作用域与内存特性)

维度 局部变量 成员变量(实例变量)
定义位置 方法 / 代码块(if/for)内部 类内部、方法外部
作用域 仅所属方法 / 代码块,外部不可访问 整个类的所有非静态方法(对象级别)
默认值 无默认值,必须手动初始化后使用 有默认值(int=0、boolean=false、引用类型 = null)
内存归属 方法栈帧(方法执行时创建,执行结束销毁) 对象堆内存(对象创建时分配,对象销毁后回收)
多方法共享方式 需传参(传递副本) 直接访问(所有方法共享同一块内存)
示例 kthSmallest中的res、k Solution类中定义的targetK、res

3. 问题代码的根因拆解

结合 “值传递” 和 “变量作用域”,分析代码失效的核心原因:

  1. k和res是kthSmallest方法的局部变量,调用dfs时传递的是 “数值副本”;

  2. dfs方法内:

    • --k修改的是 “k 的副本”,外层原始k无任何变化;
    • res=node.val修改的是 “res 的副本”,外层原始res仍为初始值 0;
  3. dfs执行结束后,外层res未被修改,因此返回 0。

三、正确解决方案与对比

方案 1:使用成员变量

核心思路

将需要跨方法共享的k和res定义为成员变量,利用 “成员变量全局共享同一块内存” 的特性,绕过值传递的副本陷阱。

class Solution {
    // 成员变量:属于Solution对象,所有方法共享同一内存
    private int targetK; // 存储目标k值
    private int res;     // 存储最终结果

    public int kthSmallest(TreeNode root, int k) {
        this.targetK = k; // 给成员变量赋值(修改原始值)
        this.res = 0;     // 初始化结果
        dfs(root);        // 递归无需传参
        return res;       // 取修改后的成员变量值
    }

    // 中序遍历:直接访问成员变量,修改的是原始值
    private void dfs(TreeNode node) {
        if (node == null) return;
        dfs(node.left);          // 递归左子树(BST左子树值更小)
        if (targetK == 0) return;// 提前终止,提升效率
        if (--targetK == 0) {    // 修改原始targetK
            res = node.val;      // 修改原始res,外层可感知
        }
        dfs(node.right);         // 递归右子树
    }
}

方案 2:使用引用类型(数组)传递

核心思路

将基本类型包装为引用类型(数组),利用 “地址副本指向同一对象” 的特性,通过修改数组内容实现跨方法传值。

class Solution {
    public int kthSmallest(TreeNode root, int k) {
        // 数组是引用类型,存储:[0] = k值,[1] = 结果值
        int[] shareData = new int[]{k, 0};
        dfs(root, shareData);
        return shareData[1]; // 取修改后的数组内容
    }

    private void dfs(TreeNode node, int[] shareData) {
        if (node == null) return;
        dfs(node.left, shareData);
        if (shareData[0] == 0) return;
        // 修改数组内容(通过地址副本找到原数组)
        if (--shareData[0] == 0) {
            shareData[1] = node.val;
        }
        dfs(node.right, shareData);
    }
}

方案对比

方案 核心原理 优点 缺点
成员变量 类级别变量共享内存,绕过值传递 代码简洁、可读性高、符合直觉 多线程场景需注意线程安全;依赖对象状态
数组(引用类型) 传递地址副本,修改对象内容 无需依赖成员变量;无线程安全问题(局部引用) 代码稍繁琐;需理解引用类型的传递逻辑

四、测试验证(确保方案有效)

测试用例

构建 BST 结构:

    3
   / \
  1   4
   \
    2

调用kthSmallest(root, 3),预期返回 3(第 3 小元素)。

public static void main(String[] args) {
    // 构建BST
    TreeNode root = new TreeNode(3);
    root.left = new TreeNode(1);
    root.right = new TreeNode(4);
    root.left.right = new TreeNode(2);

    Solution solution = new Solution();
    int result = solution.kthSmallest(root, 3);
    System.out.println(result); // 输出3,符合预期
}

五、核心结论与实践指导

1. 核心结论

2. 实践指导

3. 避坑口诀