首先,记住最关键的结论,这能帮你理解一切相关讨论:

Java中只有按值传递,没有按引用传递。

但是,这个“值”分为两种情况:

  1. 对于基本数据类型(如 int, double, char, boolean 等),传递的是变量存储的实际数值的副本
  2. 对于引用数据类型(如 Object, 数组, String 等),传递的是对象引用的副本(你可以把引用理解成对象的“地址”或“遥控器”)。

下面我们通过代码和图示来彻底理解这两种情况。


1. 基本数据类型的参数传递

当传递基本数据类型时,方法接收到的是原值的一个拷贝。在方法内部对这个参数的任何修改,都只影响这个拷贝,而不会影响原始的变量。

public class ParameterPassing {

    public static void main(String[] args) {
        int x = 10;
        int y = 20;
        System.out.println("交换前: x = " + x + ", y = " + y); // 输出:x = 10, y = 20
        
        // 尝试调用swap方法交换x和y的值
        swap(x, y);
        
        System.out.println("交换后: x = " + x + ", y = " + y); // 输出:x = 10, y = 20 (未改变!)
    }

    public static void swap(int a, int b) {
        int temp = a;
        a = b;
        b = temp;
        System.out.println("swap方法内: a = " + a + ", b = " + b); // 输出:a = 20, b = 10
    }
}

内存模型与过程分析:

  1. main方法开始:在栈内存中,为 xy 分配空间,并存储值 10 和 20。

    [栈内存]
    main的栈帧:
    x -> 10
    y -> 20
    
  2. 调用swap(x, y)

    • Java将 xy(10和20)拷贝一份,传递给 swap 方法的形参 ab
    • 此时,在栈上为 swap 方法创建新的栈帧,里面有 a, b, temp
    [栈内存]
    swap的栈帧:
    a -> 10  (x的拷贝)
    b -> 20  (y的拷贝)
    temp -> ?
    ---
    main的栈帧:
    x -> 10
    y -> 20
    
  3. 执行swap方法体

    • temp = a; -> temp 变为 10。
    • a = b; -> a 变为 20。
    • b = temp; -> b 变为 10。
    • 此时,swap 栈帧中的 ab 成功交换。
    [栈内存]
    swap的栈帧:
    a -> 20
    b -> 10
    temp -> 10
    ---
    main的栈帧:
    x -> 10 (未变!)
    y -> 20 (未变!)
    
  4. swap方法结束swap 的栈帧被销毁,其中的 a, b, temp 全部消失。程序回到 main 方法,xy 的值没有任何变化。

结论:对于基本类型,方法内对参数的修改,影响的是副本,与原变量无关。


2. 引用数据类型的参数传递

这是容易让人误解为“按引用传递”的地方。我们传递的是引用的副本,而不是对象本身。

情况一:通过引用副本修改对象状态

这种情况让人感觉像是“按引用传递”,因为方法内部对对象的修改影响了原对象。

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}

public class ParameterPassing {

    public static void main(String[] args) {
        Person person = new Person("Alice");
        System.out.println("修改前: " + person.name); // 输出:Alice
        
        // 调用方法修改person对象的name
        changeName(person);
        
        System.out.println("修改后: " + person.name); // 输出:Bob (改变了!)
    }

    public static void changeName(Person p) {
        p.name = "Bob"; // 通过引用p,修改了它指向的对象的属性
    }
}

内存模型与过程分析:

  1. main方法开始

    • person 是一个引用变量,它在栈中存储了一个地址(比如0x100)。
    • 这个地址指向堆内存中一个 Person 对象,其 name 为 “Alice”。
    [栈内存]          [堆内存]
    main的栈帧:
    person -> 0x100   ---> 0x100: Person对象 { name: "Alice" }
    
  2. 调用changeName(person)

    • Java将 person 变量存储的值(地址0x100)拷贝一份,传递给 changeName 方法的形参 p
    • 现在,personp 这两个不同的引用变量,存储着相同的地址,指向同一个对象
    [栈内存]          [堆内存]
    changeName的栈帧:
    p -> 0x100      --->
    ---                  |
    main的栈帧:           |
    person -> 0x100   ---> 0x100: Person对象 { name: "Alice" }
    
  3. 执行changeName方法体

    • p.name = "Bob"; 意味着:“找到 p 指向的那个对象(地址0x100),并将其 name 属性改为 “Bob””。
    • 由于 personp 指向的是同一个对象,所以通过 p 修改对象,person 看到的自然也是修改后的结果。
    [栈内存]          [堆内存]
    changeName的栈帧:
    p -> 0x100      --->
    ---                  |
    main的栈帧:           |
    person -> 0x100   ---> 0x100: Person对象 { name: "Bob" } <-- 对象被修改了
    
  4. changeName方法结束p 随着栈帧销毁而消失,但 person 仍然指向那个已经被修改的对象。

结论:方法可以通过传入的引用副本,修改其所指向的对象的状态。

情况二:试图改变引用本身(重新赋值)

这种情况证明了它是“按值传递”,因为方法内部无法让外部的原始引用指向一个新的对象。

public class ParameterPassing {

    public static void main(String[] args) {
        Person person = new Person("Alice");
        System.out.println("重赋值前: " + person.name); // 输出:Alice
        
        // 尝试让person指向一个新的对象
        reassignPerson(person);
        
        System.out.println("重赋值后: " + person.name); // 输出:Alice (未改变!)
    }

    public static void reassignPerson(Person p) {
        // 这里让p指向了一个全新的对象
        p = new Person("Bob");
        System.out.println("reassignPerson方法内: " + p.name); // 输出:Bob
    }
}

内存模型与过程分析:

  1. main方法开始调用reassignPerson(person) 与之前类似:

    [栈内存]          [堆内存]
    reassignPerson的栈帧:
    p -> 0x100      --->
    ---                  |
    main的栈帧:           |
    person -> 0x100   ---> 0x100: Person对象 { name: "Alice" }
    
  2. 执行reassignPerson方法体

    • p = new Person("Bob"); 这行代码创建了一个新的 Person 对象(假设在地址0x200)。
    • 它将形参 p 的值从 0x100 改为 0x200。
    • 注意:这改变的是 p 这个拷贝的引用,而 main 方法中的 person 引用仍然忠诚地指向 0x100。
    [栈内存]          [堆内存]
    reassignPerson的栈帧:
    p -> 0x200      ---> 0x200: Person对象 { name: "Bob" } (新对象)
    ---
    main的栈帧:
    person -> 0x100   ---> 0x100: Person对象 { name: "Alice" } (原对象,未受影响)
    
  3. reassignPerson方法结束p 和它指向的新对象(0x200)之间的连接断开(如果该对象没有其他引用,稍后会被GC回收)。main 方法中的 person 毫无变化。

结论:方法内部无法通过修改引用的副本来改变原始引用变量的指向。这有力地证明了是“按值传递”(传递的是引用的值)。


关于String的特殊说明

String 是一个特殊的引用类型,因为它具有不可变性。任何对String的修改(如 concat, +, replace)都会创建一个全新的String对象。

public static void changeString(String str) {
    str = "新的内容"; // 这相当于 str = new String("新的内容");
}

public static void main(String[] args) {
    String s = "原始内容";
    changeString(s);
    System.out.println(s); // 输出: "原始内容"
}

这个行为与上面的“情况二”完全相同。str = "新的内容"; 只是让方法的局部变量 str 指向了一个新对象,对外部的 s 没有影响。

总结对比表

特性基本数据类型 (如 int)引用数据类型 (如 Person, 数组)
传递的内容变量值的副本对象引用(地址)的副本
能否修改原始变量本身的值不能不能(无法让原始引用指向新对象)
能否修改原始对象的状态(不适用,无状态)(通过引用的副本操作同一个对象)
通俗比喻给你一份文件的复印件,你怎么涂改都不影响原件。给你一个遥控器(引用)的复制品。你们操作的是同一个电视(对象)。你可以换台(修改状态),但你不能通过扔掉你的复制品遥控器来让我的遥控器也失灵(改变引用本身)。

引用数据类型是一种不直接存储数据值本身,而是存储对象在内存中地址(即"引用")的数据类型。变量本身不包含对象的实际数据,而是像一个"遥控器"或"名片",告诉你如何找到内存中真正的对象。


引用数据类型 vs 基本数据类型

为了更好地理解,我们先对比一下它们:

特性基本数据类型引用数据类型
存储内容直接存储数据值本身存储对象在内存中的地址(引用)
内存位置变量和值都存储在栈内存引用(变量名)在栈内存,实际对象在堆内存
默认值有默认值(如 int 是 0,boolean 是 false)默认值为 null(表示不指向任何对象)
示例类型int, double, char, boolean 等8种String, 数组, 自定义类(如 Person), Integer
赋值操作创建值的副本创建引用的副本(多个变量可指向同一个对象)
比较操作== 比较值是否相等== 比较内存地址是否相同(是否同一个对象)

引用数据类型的具体类型

    • 系统预定义类:如 StringIntegerScannerFile 等。
    • 自定义类:你自己创建的类,如 PersonStudentCar 等。
  1. 接口

    • ListMapRunnable 等。虽然不能直接实例化,但用于声明引用变量,指向实现了该接口的类的对象。
  2. 数组

    • 无论是基本类型的数组(如 int[])还是对象数组(如 String[]),数组本身都是引用数据类型。

内存模型详解(核心)

这是理解引用数据类型最关键的部分。我们通过一个例子来看:

public class ReferenceExample {
    public static void main(String[] args) {
        // 基本数据类型
        int age = 25;

        // 引用数据类型 - String
        String name = "Alice";

        // 引用数据类型 - 自定义类
        Person person = new Person("Bob", 30);

        // 引用数据类型 - 数组
        int[] scores = {90, 85, 95};
    }
}

class Person {
    String name;
    int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

内存中的状态如下图所示:

[栈内存 (Stack)]                    [堆内存 (Heap)]
--------------------------------    --------------------------------
main方法的栈帧:
- age: 25 (基本类型,值直接存储)     

- name: 0x100 ---> [String对象 "Alice"] 在字符串常量池

- person: 0x200 ---> [Person对象] 
                         |- name: 0x300 ---> [String对象 "Bob"]
                         |- age: 30

- scores: 0x400 ---> [int[]数组对象]
                         |-> index0: 90
                         |-> index1: 85
                         |-> index2: 95

解释:

  • 栈内存:存储方法调用信息和局部变量。

    • age 是基本类型,其值 25 直接存储在栈上。
    • name, person, scores 是引用变量,它们在栈上存储的是一个内存地址(如 0x100, 0x200, 0x400)。
  • 堆内存:存储所有对象和数组的实际数据。

    • 当执行 new Person(...)new int[...] 时,就在堆内存中开辟空间创建对象。
    • 引用变量通过存储的地址"指向"堆中的对象。

引用数据类型的关键行为

1. 多个引用可以指向同一个对象
Person p1 = new Person("Charlie", 25);
Person p2 = p1; // p2 和 p1 指向同一个对象

p2.age = 26; // 通过 p2 修改对象
System.out.println(p1.age); // 输出 26,因为 p1 也指向同一个对象

内存模型:

[栈]          [堆]
p1 -> 0x500 ---> [Person对象] { name: "Charlie", age: 26 }
p2 -> 0x500 ---/
2. 使用 ==equals() 的比较
  • == 比较的是引用(内存地址)是否相同。
  • equals() 比较的是对象的内容是否逻辑相等(默认行为同 ==,但可被重写,如 String)。
String s1 = new String("Hello");
String s2 = new String("Hello");
String s3 = s1;

System.out.println(s1 == s2);      // false,不同对象,地址不同
System.out.println(s1 == s3);      // true,指向同一个对象
System.out.println(s1.equals(s2)); // true,String重写了equals,比较内容
3. 特殊值 null

null 表示引用不指向任何对象。

Person p = null; // p 不指向任何对象
System.out.println(p); // 输出 null
// p.name = "John";   // 这里会抛出 NullPointerException

为什么需要引用数据类型?

c

  1. 动态内存管理:对象在堆上动态创建,生命周期更灵活。
  2. 复杂数据结构:可以创建包含多个属性和方法的复杂对象。
  3. 面向对象特性:支持继承、多态等面向对象的核心特性。
  4. 共享数据:多个引用可以指向同一个对象,方便数据共享。
  5. 高效内存使用:传递对象时只需传递引用(地址),而不需要复制整个对象。

总结

引用数据类型是Java面向对象编程的基石。理解它的关键在于分清"引用"和"对象":

  • 引用是变量,存储在栈上,内容是对象的地址。
  • 对象是实际的数据结构,存储在堆上。
  • 一个对象可以被多个引用指向。
  • 操作引用实际上是通过地址间接操作堆上的对象。

c

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐