跳到内容

元数据卡

  • 前置知识:方法定义与调用(第5章)
  • 预计时间:15 分钟
  • 阅读模式: 高度专注
  • 完成标志:能画出简单调用栈,理解值传递

本章分层

  • 必读:调用栈帧结构与压栈/弹栈过程
  • 选读:值传递 vs 引用对象的区别
  • 进阶:无

第五幕:藏在背后的高塔

你调用方法的时候,电脑做了什么?

不是"跑到另一个地方执行",不是"跳过去"——是一个精确的、可预测的结构:你每次调用方法,JVM 在当前线程的调用栈上压入一个栈帧

栈帧 = 一个方法调用的"快照":局部变量表、操作数栈、返回地址。

打个比方:你走进老陈的铁匠铺,把剑坯递过去(压栈)。老陈打完了把剑还给你(返回值)。你走出铁匠铺(出栈),回到原来的位置。如果你在 forge 里又调用了 checkMaterial——老陈得先让你去找检验员,检验完了回来,他再继续打。这就形成了一座高塔

main 调用 forge("钢", 5)

 ├─ 压入栈帧:forge("钢", 5)
 │  执行到一半,发现需要检查材料强度

 ├─ 压入栈帧:checkStrength("钢")
 │  执行完毕,返回 "硬度8"
 │  弹出栈帧 ← 检查员走了

 ├─ 继续执行 forge,拿到结果
 │  执行完毕,返回 "精良钢剑"
 │  弹出栈帧 ← 铁匠也走了

 └─ main 继续

第六幕:用代码看调用栈

java
public class StackDemo {
    public static void main(String[] args) {
        System.out.println("1. main 开始");
        int result = add(3, 4);
        System.out.println("4. result = " + result);
        System.out.println("5. main 结束");
    }
    static int add(int a, int b) {
        System.out.println("2. add 开始: a=" + a + " b=" + b);
        int sum = a + b;
        System.out.println("3. add 结束 sum=" + sum);
        return sum;
    }
}

预期输出1. main 开始 → 2. add 开始 → 3. add 结束 → 4. result = 7 → 5. main 结束

输出顺序反映调用栈的压入弹出顺序。嵌套更深时:

java
public class StackDepth {
    public static void main(String[] args) {
        System.out.println("A: main"); first(); System.out.println("F: main");
    }
    static void first() {
        System.out.println("B: first"); second(); System.out.println("E: first");
    }
    static void second() {
        System.out.println("C: second"); third(); System.out.println("D: second");
    }
    static void third() {
        System.out.println("D: third");
    }
}

预期输出A → B → C → D → D → E → F

A→B→C→D→(最深的先结束)→D→E→F。栈是后进先出——最后调用的方法最先执行完并弹出。

第七幕:值传递的真相

Java 只有值传递(pass-by-value)。你传给方法的任何东西都是它的副本。

java
public class ValuePass {
    public static void main(String[] args) {
        int x = 10;
        System.out.println("调用前: x = " + x);
        changeValue(x);
        System.out.println("调用后: x = " + x);  // 还是 10!
    }

    static void changeValue(int num) {
        num = 100;  // 改的是副本
        System.out.println("方法内: num = " + num);
    }
}

预期输出

调用前: x = 10
方法内: num = 100
调用后: x = 10

x 没有被方法改掉。因为 numx 的值的一个复制品,它们在内存里的位置不同。

"那数组和对象呢?"你问。

java
import java.util.Arrays;

public class ValuePassRef {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        System.out.println("调用前: " + Arrays.toString(arr));
        changeArray(arr);
        System.out.println("调用后: " + Arrays.toString(arr));  // 变了!
    }

    static void changeArray(int[] nums) {
        nums[0] = 99;  // 改的是数组里的元素
    }
}

预期输出

调用前: [1, 2, 3]
调用后: [99, 2, 3]

"数组被改了!这难道不是引用传递吗?"

不是。 你可以改数组内容(副本指向同一个数组对象),但不能让 nums 指向新数组来改变 main 里的 arr。因为 nums 本身是 arr地址值的副本

用老陈的话说:"你拿着地图的副本可以找到那座山,但你换一张地图,原来的地图不会跟着变。"

陷阱:无限递归——栈击穿了

java
public class Boom {
    public static void main(String[] args) {
        dive();  // 崩溃
    }

    static void dive() {
        System.out.println("下潜…");
        dive();  // 又调自己
    }
}

运行后你会看到一长串 "下潜…",然后报错:

Exception in thread "main" java.lang.StackOverflowError

调用栈是有限的。 每个栈帧占一点内存(几十到几百字节)。无限压栈,最终栈内存耗尽,JVM 扔出一句 StackOverflowError

实战教训:写递归一定要有终止条件。每调用一次方法,栈帧就多一层——栈帧不是无限的。

旅人笔记

每次方法调用,JVM 在调用栈上压入一个栈帧;调用结束后弹出。Java 只有值传递——你传的是副本。调用栈是后进先出的高塔,记住一句话:你看到的变量名只是名字,栈帧之间是隔离的。

下一站:方法重载

你已经能把任务拆成方法了。但如果你想让两个方法做相似但不同的事,却不想取两个不同的名字呢?就像老陈的锻造——传进来的材料不同,打出来的东西自然不同。下一节,方法重载

Built with VitePress | Software Systems Atlas