Java是“按引用传递”还是“按值传递”?

我一直认为Java使用逐引用传递

但是,我看到一篇博客文章声称Java使用按值传递

我不认为我理解他们所做的区分。

这是什么解释?


答案 1

术语“按值传递”和“按引用传递”在计算机科学中具有特殊、精确定义的含义。这些含义与许多人第一次听到这些术语时的直觉不同。这次讨论中的大部分混乱似乎都来自这一事实。

术语“按值传递”和“按引用传递”正在讨论变量。按值传递意味着变量的值被传递给函数/方法。按引用传递意味着对该变量的引用将传递给函数。后者为函数提供了一种更改变量内容的方法。

根据这些定义,Java始终是按值传递的。不幸的是,当我们处理保存对象的变量时,我们实际上是在处理称为引用的对象句柄,这些柄也是按值传递的。这个术语和语义很容易让许多初学者感到困惑。

它是这样的:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    Dog oldDog = aDog;

    // we pass the object to foo
    foo(aDog);
    // aDog variable is still pointing to the "Max" dog when foo(...) returns
    aDog.getName().equals("Max"); // true
    aDog.getName().equals("Fifi"); // false
    aDog == oldDog; // true
}

public static void foo(Dog d) {
    d.getName().equals("Max"); // true
    // change d inside of foo() to point to a new Dog instance "Fifi"
    d = new Dog("Fifi");
    d.getName().equals("Fifi"); // true
}

在上面的示例中仍将返回 。在函数中,当对象引用按值传递时,不会更改其中的值。如果它是通过引用传递的,则在调用 后,in 将返回。aDog.getName()"Max"aDogmainfooDog"Fifi"aDog.getName()main"Fifi"foo

同样:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    Dog oldDog = aDog;

    foo(aDog);
    // when foo(...) returns, the name of the dog has been changed to "Fifi"
    aDog.getName().equals("Fifi"); // true
    // but it is still the same dog:
    aDog == oldDog; // true
}

public static void foo(Dog d) {
    d.getName().equals("Max"); // true
    // this changes the name of d to be "Fifi"
    d.setName("Fifi");
}

在上面的示例中,是狗的名字在调用后,因为对象的名称是在 .对 执行的任何操作都是这样,出于所有实际目的,它们都是在 上执行的,但不可能更改变量本身的值。Fififoo(aDog)foo(...)foodaDogaDog

有关按引用传递和按值传递的详细信息,请参阅以下答案:https://stackoverflow.com/a/430958/6005228。这更彻底地解释了两者背后的语义和历史,也解释了为什么Java和许多其他现代语言在某些情况下似乎同时做到了这两点。


答案 2

我刚刚注意到你引用了我的文章

Java Spec说Java中的所有内容都是按值传递的。Java中没有“通过引用传递”这样的东西。

理解这一点的关键是,像这样的东西

Dog myDog;

不是狗;它实际上是指向狗的指针。在Java中使用术语“引用”是非常具有误导性的,并且是导致这里大多数混淆的原因。他们所谓的“引用”的行为/感觉更像是我们在大多数其他语言中所谓的“指针”。

这意味着,当你有

Dog myDog = new Dog("Rover");
foo(myDog);

您实际上是将创建对象的地址传递给方法。Dogfoo

(我这么说主要是因为Java指针/引用不是直接地址,但以这种方式思考它们是最简单的。

假设对象驻留在内存地址 42。这意味着我们将 42 传递给该方法。Dog

如果将方法定义为

public void foo(Dog someDog) {
    someDog.setName("Max");     // AAA
    someDog = new Dog("Fifi");  // BBB
    someDog.setName("Rowlf");   // CCC
}

让我们来看看发生了什么。

  • 参数设置为值 42someDog
  • 在“AAA”行
    • someDog跟随到它指向(地址 42 处的对象)DogDog
    • (地址42中的那个)被要求将他的名字更改为Max。Dog
  • 在行“BBB”
    • 将创建一个新的。假设他在地址74Dog
    • 我们将参数分配给 74someDog
  • 在“CCC”行
    • someDog 被跟踪到它指向(地址 74 处的对象)DogDog
    • (地址74的那个)被要求将他的名字改为RowlfDog
  • 然后,我们返回

现在让我们考虑一下方法之外会发生什么:

myDog改变了吗?

这是关键。

请记住,这是一个指针,而不是一个实际的,答案是否定的。 仍然具有值 42;它仍然指向原始版本(但请注意,由于行“AAA”,它的名字现在是“Max” - 仍然是同一个狗;的值未更改。myDogDogmyDogDogmyDog

遵循地址并更改其末尾的内容是完全有效的;但是,这不会更改变量。

Java的工作方式与C完全相同。您可以分配指针,将指针传递给方法,跟随方法中的指针并更改指向的数据。但是,调用方将看不到您对该指针指向的位置所做的任何更改。(在具有按引用传递语义的语言中,方法函数可以更改指针,调用方将看到该更改。

在C++、Ada、Pascal 和其他支持按引用传递的语言中,您实际上可以更改传递的变量。

如果Java具有逐引用传递语义,那么我们上面定义的方法在BBB行上分配时会改变指向的位置。foomyDogsomeDog

将引用参数视为传入变量的别名。分配该别名后,传入的变量也是如此。

更新

评论中的讨论值得一些澄清...

在C中,您可以编写

void swap(int *x, int *y) {
    int t = *x;
    *x = *y;
    *y = t;
}

int x = 1;
int y = 2;
swap(&x, &y);

这在 C 中不是特例。这两种语言都使用按值传递语义。在这里,调用站点正在创建其他数据结构,以帮助函数访问和操作数据。

该函数正在向数据传递指针,并遵循这些指针来访问和修改该数据。

Java中的类似方法,其中调用方设置辅助结构,可能是:

void swap(int[] x, int[] y) {
    int temp = x[0];
    x[0] = y[0];
    y[0] = temp;
}

int[] x = {1};
int[] y = {2};
swap(x, y);

(或者,如果您希望这两个示例都演示其他语言没有的功能,请创建一个可变的IntWrapper类来代替数组)

在这些情况下,C 和 Java 都在模拟按引用传递。它们仍然在传递值(指向整数或数组的指针),并在被调用的函数中跟随这些指针来操作数据。

通过引用传递是关于函数声明/定义,以及它如何处理其参数。引用语义适用于对该函数的每次调用,并且调用站点只需要传递变量,不需要额外的数据结构。

这些模拟需要调用站点和函数进行协作。毫无疑问,它是有用的,但它仍然是按值传递的。