Java 零基础教程

Java 按值传递

本章将深入探讨按值传递的细节,解释它如何影响不同的数据类型,以及它如何左右程序的行为。

1. 深入理解按值传递

在 Java 中,所有的参数都是按值传递(by value)给方法的。这意味着,当你使用一个参数调用方法时,系统会创建该参数值的一个副本(拷贝),并将这个副本传递给方法。方法随后在这个副本上进行操作,而不是操作原始变量本身。

极其重要的一点是要明白这个区别,因为它决定了在方法内部所做的修改是否会影响到方法外部的原始变量。

2. 基本数据类型

当你将基本数据类型(如 intdoubleboolean 等)传递给方法时,方法接收的是存储在该变量中的实际数值的副本。在方法内部对该参数所做的任何修改,都不会影响到方法外部的原始变量。

public class PassByValuePrimitive {
    public static void main(String[] args) {
        int x = 10;
        System.out.println("调用 modifyValue 之前: x = " + x); // 输出: 10
        modifyValue(x);
        System.out.println("调用 modifyValue 之后: x = " + x);  // 输出: 10
    }

    public static void modifyValue(int num) {
        num = 20;
        System.out.println("在 modifyValue 内部: num = " + num);      // 输出: 20
    }
}

在这个例子中,x 被初始化为 10。modifyValue 方法接收了数值 10 的一个副本,将其赋值给局部变量 num,然后将 num 修改为 20。然而,main 方法中的原始变量 x 保持不变。x 的值仍然是 10,因为 modifyValue 操作的只是 x 数值的一个副本。

3. 引用数据类型(对象)

当你将引用数据类型(一个对象)传递给方法时,传递的是引用的副本。它不是对象本身的副本。这意味着,原始引用和被复制的引用在内存中指向的是同一个对象

因此,如果方法修改了对象的状态(例如,更改了对象某个字段的值),这些修改反映在原始对象上,因为两个引用指向的是同一个实体。但是,如果方法将引用参数重新赋值,让它指向一个完全不同的新对象,原始引用将保持不变,依然指向最初的那个对象。

class Dog {
    String name;

    public Dog(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

public class PassByValueReference {
    public static void main(String[] args) {
        Dog myDog = new Dog("巴迪");
        System.out.println("调用 changeName 之前: " + myDog.getName()); // 输出: 巴迪
        changeName(myDog);
        System.out.println("调用 changeName 之后: " + myDog.getName());  // 输出: 麦克斯

        Dog yourDog = new Dog("贝拉");
        System.out.println("调用 replaceDog 之前: " + yourDog.getName()); // 输出: 贝拉
        replaceDog(yourDog);
        System.out.println("调用 replaceDog 之后: " + yourDog.getName());  // 输出: 贝拉
    }

    public static void changeName(Dog dog) {
        dog.setName("麦克斯");
        System.out.println("在 changeName 内部: " + dog.getName());      // 输出: 麦克斯
    }

    public static void replaceDog(Dog dog) {
        dog = new Dog("查理"); // 重新赋值引用
        System.out.println("在 replaceDog 内部: " + dog.getName());      // 输出: 查理
    }
}
  • changeName 的例子中,changeName 方法接收了指向 myDog 对象的引用副本。随后,setName 方法在两个引用共同指向的那个对象上被调用。因此,当在 main 方法中调用 myDog.getName() 时,修改会体现出来。
  • replaceDog 的例子中,replaceDog 方法接收了指向 yourDog 对象的引用副本。然而在方法内部,dog 引用被重新赋值,指向了一个名为 "查理" 的 Dog 对象。这种重新赋值不会影响 main 方法中的原始 yourDog 引用,它依然死死地指向原来的 "贝拉" 对象。

4. String 对象:一个特例

Java 中的 String 对象是不可变的(immutable),这意味着它们的状态在创建后就无法被修改。当你将一个 String 传递给方法并尝试修改它时,你实际上是在创建一个新的 String 对象。原始的 String 保持原样。

public class PassByValueString {
    public static void main(String[] args) {
        String text = "Hello";
        System.out.println("调用 modifyString 之前: " + text); // 输出: Hello
        modifyString(text);
        System.out.println("调用 modifyString 之后: " + text);  // 输出: Hello
    }

    public static void modifyString(String str) {
        str = str + " World";  // 创建了一个新的 String 对象
        System.out.println("在 modifyString 内部: " + str);      // 输出: Hello World
    }
}

在这个例子中,modifyString 方法表面上看起来修改了 text 字符串。但由于字符串是不可变的,代码 str = str + " World"; 实际上创建了一个全新String 对象 "Hello World",并将其引用赋值给了局部变量 strmain 方法中的原始 text 变量依然指向原始的 "Hello" 字符串。

5. 综合实战演练

让我们探索更多实际的例子,来巩固对 Java 中按值传递的理解。

5.1 示例 1:修改数组

数组属于引用类型,因此理解按值传递如何影响它们非常关键。

public class PassByValueArray {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        System.out.print("调用 modifyArray 之前: ");
        printArray(numbers); // 输出: 1 2 3
        
        modifyArray(numbers);
        
        System.out.print("调用 modifyArray 之后: ");
        printArray(numbers);  // 输出: 10 2 3
    }

    public static void modifyArray(int[] arr) {
        arr[0] = 10; // 修改索引为 0 的元素
        System.out.print("在 modifyArray 内部: ");
        printArray(arr);      // 输出: 10 2 3
    }

    public static void printArray(int[] arr) {
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

在这种情况下,modifyArray 方法接收了指向 numbers 数组的引用副本。当执行 arr[0] = 10; 时,它修改了内存中的实际数组,因为 main 中的 numbersmodifyArray 中的 arr 都指向同一个数组对象。

5.2 示例 2:操作自定义对象

让我们再次回顾 Dog 类,并扩展它的功能以进一步演示按值传递。

class Dog {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{name='" + name + "', age=" + age + '}';
    }
}

public class PassByValueDog {
    public static void main(String[] args) {
        Dog myDog = new Dog("巴迪", 3);
        System.out.println("调用 celebrateBirthday 之前: " + myDog); // 输出: Dog{name='巴迪', age=3}
        celebrateBirthday(myDog);
        System.out.println("调用 celebrateBirthday 之后: " + myDog);  // 输出: Dog{name='巴迪', age=4}

        Dog yourDog = new Dog("贝拉", 5);
        System.out.println("调用 changeDog 之前: " + yourDog); // 输出: Dog{name='贝拉', age=5}
        changeDog(yourDog, new Dog("查理", 2));
        System.out.println("调用 changeDog 之后: " + yourDog);  // 输出: Dog{name='贝拉', age=5}
    }

    public static void celebrateBirthday(Dog dog) {
        dog.setAge(dog.getAge() + 1);
        System.out.println("在 celebrateBirthday 内部: " + dog);      // 输出: Dog{name='巴迪', age=4}
    }

    public static void changeDog(Dog originalDog, Dog newDog) {
        originalDog = newDog; // 重新赋值引用
        System.out.println("在 changeDog 内部: " + originalDog);      // 输出: Dog{name='查理', age=2}
    }
}

在这里,celebrateBirthday 成功修改了 Dog 对象的年龄,因为方法接收了引用的副本,且两个引用指向同一个对象。然而,changeDog 试图将 originalDog 引用重新赋值为一个新的 Dog 对象,这种重新赋值只影响了方法内部的局部引用 originalDog,并没有改变 main 方法中的 yourDog 引用。