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

我一直认为 Java 是通过引用传递的

但是,我已经看到一些博客文章(例如this blog )声称不是。

我不认为我能理解他们的区别。

有什么解释?

答案

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
}

在上面的示例中, aDog.getName()仍将返回"Max"main的值aDog不会在功能foo中用Dog "Fifi"更改,因为对象引用是通过值传递的。如果通过引用传递,则mainaDog.getName()将在对foo的调用之后返回"Fifi"

同样地:

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");
}

在上面的示例中, Fifi是调用foo(aDog)之后的狗的名字,因为该对象的名称是在foo(...)内部设置的。任何操作是foo上执行d是这样的,对于所有的实际目的,它们被执行aDog ,但它是不可能改变的变量的值aDog本身。

我只是注意到您引用了我的文章

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

理解这一点的关键是

Dog myDog;

不是狗它实际上是指向狗的指针

这意味着什么时候

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

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

(我说的主要是因为 Java 指针不是直接地址,但以这种方式想到它们最简单)

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

如果方法定义为

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

让我们看看发生了什么。

  • 参数someDog设置为值 42
  • 在 “AAA” 行
    • someDog之后到Dog它指向(在Dog对象在地址 42)
    • 那只Dog (地址为 42 的那只Dog )被要求将他的名字改成 Max
  • 在 “BBB” 行
    • 创建了一条新的Dog 。假设他在地址 74
    • 我们将参数someDog分配给 74
  • 在 “CCC” 行
    • someDog 之后到Dog它指向(在Dog对象在地址 74)
    • 那只Dog (地址为 74 的那只Dog )被要求将他的名字改成 Rowlf
  • 然后,我们返回

现在,让我们考虑一下方法外发生的情况:

我的myDog变了吗?

有钥匙

请记住, myDog是一个指针 ,而不是实际的Dog ,答案是否定的。 myDog的值仍然为 42;它仍指向原始的Dog (但请注意,由于行 “AAA”,其名称现在为 “Max”- 仍是同myDog Dog; myDog的值未更改。)

跟随地址并更改地址末尾是完全有效的;但这不会更改变量。

Java 的工作原理与 C 完全相同。您可以分配一个指针,将指针传递给方法,在该方法中跟随指针并更改所指向的数据。但是,您不能更改该指针指向的位置。

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

要是 Java 通 - by-reference 语义中, foo我们在上面定义的方法会改变其中myDog指着时分配someDog上线 BBB。

将参考参数视为传入变量的别名。分配别名后,传入变量也将被分配。

Java 总是按值传递参数,而不是按引用传递参数。


让我通过一个例子解释一下:

public class Main {

     public static void main(String[] args) {
          Foo f = new Foo("f");
          changeReference(f); // It won't change the reference!
          modifyReference(f); // It will modify the object that the reference variable "f" refers to!
     }

     public static void changeReference(Foo a) {
          Foo b = new Foo("b");
          a = b;
     }

     public static void modifyReference(Foo c) {
          c.setAttribute("c");
     }

}

我将逐步解释这一点:

  1. 声明一个名为f的引用,类型为Foo并为其分配一个具有属性"f"的类型为Foo的新对象。

    Foo f = new Foo("f");

    在此处输入图片说明

  2. 从方法方面,声明了名称为a的类型为Foo的引用,并将其初始分配为null

    public static void changeReference(Foo a)

    在此处输入图片说明

  3. 调用方法changeReference ,将为引用a分配对象,该对象作为参数传递。

    changeReference(f);

    在此处输入图片说明

  4. 声明一个名为b的引用,类型为Foo并为其分配一个具有属性"b"的类型为Foo的新对象。

    Foo b = new Foo("b");

    在此处输入图片说明

  5. a = b对属性为"b"的对象的引用a 而不是 f进行新赋值。

    在此处输入图片说明

  6. 当您调用modifyReference(Foo c)方法时,将创建引用c并为该对象分配属性"f"

    在此处输入图片说明

  7. c.setAttribute("c");会更改引用c指向它的对象的属性,并且它是引用f指向它的对象。

    在此处输入图片说明

我希望您现在了解如何将对象作为参数传递给 Java :)

这将使您对 Java 的实际工作方式有一些见解,以至于在下一次有关 Java 通过引用传递或通过值传递的讨论中,您只会微笑:-)

第一步,请您先清除以 “p”“_ _ _ _ _ _ _ _ _” 开头的单词,尤其是如果您来自其他编程语言。 Java 和'p' 不能写在同一本书,论坛甚至 txt 中。

第二步要记住,当您将对象传递给方法时,您传递的是对象引用而不是对象本身。

  • 学生 :硕士,这是否意味着 Java 是按引用传递的?
  • 主人 :蚱 hopper,没有

现在考虑一下对象的引用 / 变量的作用 / 是:

  1. 变量包含一些位,这些位告诉 JVM 如何获取内存中的引用对象(堆)。
  2. 将参数传递给方法时, 您不是传递参考变量,而是传递参考变量中位的副本 。像这样:3bad086a。 3bad086a 表示一种获取传递的对象的方法。
  3. 因此,您只是传递 3bad086a,它是引用的值。
  4. 您传递的是引用的值,而不是引用本身(而不是对象)的值。
  5. 该值实际上已复制并提供给方法

在以下内容中(请不要尝试编译 / 执行此操作...):

1. Person person;
2. person = new Person("Tom");
3. changeName(person);
4.
5. //I didn't use Person person below as an argument to be nice
6. static void changeName(Person anotherReferenceToTheSamePersonObject) {
7.     anotherReferenceToTheSamePersonObject.setName("Jerry");
8. }

怎么了?

  • 变量person是在第 1 行中创建的,开头是 null。
  • 在第 2 行中创建一个新的 Person 对象,并将其存储在内存中,然后为该变量person提供对该 Person 对象的引用。即是它的地址。假设 3bad086a。
  • 持有对象地址的变量人员将在第 3 行中传递给函数。
  • 在第 4 行中,您可以听到寂静的声音
  • 检查第 5 行的评论
  • 创建方法局部变量-anotherReferenceToTheSamePersonObject- ,然后在第 6 行出现魔术:
    • 可变 / 参考被复制逐位和传递给anotherReferenceToTheSamePersonObject在函数内。
    • 没有创建新的 Person 实例。
    • person ” 和 “ anotherReferenceToTheSamePersonObject ” 都具有相同的值 3bad086a。
    • 不要尝试此操作,但是 person == anotherReferenceToTheSamePersonObject 将为 true。
    • 这两个变量都具有引用的相同副本,并且都引用相同的 Person 对象,Heap 上的 SAME 对象和 NOT COPY。

一张图片胜过千言万语:

价值传递

请注意,anotherReferenceToTheSamePersonObject 箭头指向对象而不是变量人!

如果您没有得到它,那就请相信我,并记住,最好说Java 是通过价值传递的 。好吧, 通过参考值 。哦,更好的是通过变量值的传递! ;)

现在,请随时恨我,但请注意,鉴于此在谈论方法参数时传递原始数据类型和对象之间没有区别

您总是传递参考值的位的副本!

  • 如果是原始数据类型,则这些位将包含原始数据类型本身的值。
  • 如果它是一个对象,则这些位将包含告诉 JVM 如何获取该对象的地址值。

Java 是按值传递的,因为您可以在方法内部随意修改所引用的对象,但是无论尝试多么努力,您都永远无法修改将保持引用的传递变量(而不是 p _ _ _ _ _ _ _)相同的对象,无论如何!


上面的 changeName 函数将永远无法修改传递的引用的实际内容(位值)。换句话说,changeName 不能使 Person 人引用另一个 Object。


当然,您可以简而言之,只是说Java 是按价值传递的!

Java 的始终是按值传递,没有例外, 永远

那么,如何让所有人对此感到困惑,以为 Java 是通过引用传递的呢,还是认为他们有一个 Java 充当引用传递的示例呢?关键是,在任何情况下,Java 都不提供对对象本身的值的直接访问。对对象的唯一访问是通过对该对象的引用 。因为 Java 对象始终通过引用而不是直接访问来访问,所以通常将字段和变量以及方法参数称为对象 ,而在学究上它们仅是对象的引用混淆源于命名上的这种变化(严格来说,是错误的)。

因此,在调用方法时

  • 对于基本参数( intlong等),按值传递是基本参数的实际值 (例如 3)。
  • 对于对象,按值传递是对对象的引用的值。

因此,如果您有doSomething(foo)public void doSomething(Foo foo) { .. }则两个 Foos 都复制了指向相同对象的引用

自然地,通过值传递对对象的引用看起来非常像(实际上在实践中是无法区分的)通过引用传递对象。

Java 通过值传递引用。

因此,您无法更改传入的引用。

我觉得争论 “按引用传递与按值传递” 不是很有帮助。

如果您说 “Java 无所不包(引用 / 值)”,则无论哪种情况,您都无法提供完整的答案。这是一些其他信息,它们有望帮助您了解内存中发生的情况。

在进入 Java 实现之前,堆栈 / 堆的崩溃过程是:值以一种井井有条的方式进出堆栈,就像自助餐厅里的一堆盘子。堆中的内存(也称为动态内存)是杂乱无章且杂乱无章的。 JVM 会尽可能地找到空间,并释放它,因为不再需要使用它的变量。

好的。首先,本地基元进入堆栈。所以这段代码:

int x = 3;
float y = 101.1f;
boolean amIAwesome = true;

结果:

堆栈上的基元

声明和实例化对象时。实际的对象在堆上。堆栈上有什么?堆上对象的地址。 C ++ 程序员将其称为指针,但是一些 Java 开发人员反对 “指针” 一词。随你。只知道对象的地址在堆栈上。

像这样:

int problems = 99;
String name = "Jay-Z";

一个b * 7ch不是一个!

数组是一个对象,因此它也在堆上。那数组中的对象呢?他们获得了自己的堆空间,每个对象的地址进入数组内部。

JButton[] marxBros = new JButton[3];
marxBros[0] = new JButton("Groucho");
marxBros[1] = new JButton("Zeppo");
marxBros[2] = new JButton("Harpo");

马克思兄弟

那么,调用方法时传递的是什么?如果传入一个对象,则实际上传递的是该对象的地址。有些人可能会说地址的 “值”,而有些人说这只是对对象的引用。这是 “参考” 和“价值” 支持者之间圣战的起源。您所说的并不重要,因为您了解要传递的是对象的地址。

private static void shout(String name){
    System.out.println("There goes " + name + "!");
}

public static void main(String[] args){
    String hisName = "John J. Jingleheimerschmitz";
    String myName = hisName;
    shout(myName);
}

创建一个 String 并在堆中为其分配空间,并将该字符串的地址存储在堆栈中,并hisName指定标识符hisName ,因为第二个 String 的地址与第一个 String 相同,因此不会创建新的 String 并且没有分配新的堆空间,但是在堆栈上创建了一个新的标识符。然后我们调用shout() :创建一个新的堆栈框架,并创建一个新的标识符, name ,并为其分配已经存在的 String 的地址。

拉达迪达达达达

那么,价值,参考?您说 “土豆”。

为了显示对比,请比较以下C ++Java 代码段:

在 C ++ 中: 注意:错误代码 - 内存泄漏!但这说明了这一点。

void cppMethod(int val, int &ref, Dog obj, Dog &objRef, Dog *objPtr, Dog *&objPtrRef)
{
    val = 7; // Modifies the copy
    ref = 7; // Modifies the original variable
    obj.SetName("obj"); // Modifies the copy of Dog passed
    objRef.SetName("objRef"); // Modifies the original Dog passed
    objPtr->SetName("objPtr"); // Modifies the original Dog pointed to 
                               // by the copy of the pointer passed.
    objPtr = new Dog("newObjPtr");  // Modifies the copy of the pointer, 
                                   // leaving the original object alone.
    objPtrRef->SetName("objRefPtr"); // Modifies the original Dog pointed to 
                                    // by the original pointer passed. 
    objPtrRef = new Dog("newObjPtrRef"); // Modifies the original pointer passed
}

int main()
{
    int a = 0;
    int b = 0;
    Dog d0 = Dog("d0");
    Dog d1 = Dog("d1");
    Dog *d2 = new Dog("d2");
    Dog *d3 = new Dog("d3");
    cppMethod(a, b, d0, d1, d2, d3);
    // a is still set to 0
    // b is now set to 7
    // d0 still have name "d0"
    // d1 now has name "objRef"
    // d2 now has name "objPtr"
    // d3 now has name "newObjPtrRef"
}

在 Java 中,

public static void javaMethod(int val, Dog objPtr)
{
   val = 7; // Modifies the copy
   objPtr.SetName("objPtr") // Modifies the original Dog pointed to 
                            // by the copy of the pointer passed.
   objPtr = new Dog("newObjPtr");  // Modifies the copy of the pointer, 
                                  // leaving the original object alone.
}

public static void main()
{
    int a = 0;
    Dog d0 = new Dog("d0");
    javaMethod(a, d0);
    // a is still set to 0
    // d0 now has name "objPtr"
}

Java 仅具有两种传递类型:内置类型按值传递,对象类型按指针值传递。

Java 按值传递对对象的引用。

基本上,重新分配 Object 参数不会影响参数,例如,

private void foo(Object bar) {
    bar = null;
}

public static void main(String[] args) {
    String baz = "Hah!";
    foo(baz);
    System.out.println(baz);
}

将打印出"Hah!"而不是null 。之所以起作用,是因为barbaz值的副本,而baz只是对"Hah!"的引用"Hah!" 。如果它是实际的引用本身,那么foo会将baz重新定义为null