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

7757

我一直认为Java使用的是按引用传递。然而,我看了一篇博客文章,声称Java使用的是按值传递。我不认为我理解作者所做的区分。

这是什么意思?


7
我们更常用的说法是,一个“按引用传递”的变量可以被改变。这个术语出现在教科书中,因为语言理论家需要一种区分原始数据类型(int、bool、byte)和复杂结构对象(数组、流、类)的方法,也就是说,那些可能具有无限内存分配的对象。 - jlau
7
我想提醒您,在大多数情况下不需要考虑这个问题。我在编程中使用Java很多年,直到学习了C++之前,我完全不知道传递引用和传递值是什么意思。直到那时,我的直觉解决方案总是有效的,这就是为什么Java是最适合初学者的语言之一。因此,如果您目前担心您的函数需要引用还是值,请按原样传递它,您会没问题的。 - Tobias
73
Java通过值传递引用。 - The Student
24
简单来说,这种混淆是因为在Java中,所有非基本数据类型都是通过"引用"来处理/访问的。然而,传递始终是按值进行的。因此,对于所有非基本类型,引用是按其值传递的。所有基本类型也都是按值传递的。 - Ozair Kafray
6
我觉得这篇文章非常有帮助: https://www.baeldung.com/java-pass-by-value-or-pass-by-reference - Natasha Kurian
显示剩余16条评论
95个回答

7009
术语“按值传递”和“按引用传递”在计算机科学中有特定且精确定义的含义。这些含义与初听到这些术语时许多人的直觉不同。本讨论中的许多混乱似乎来自于此事实。
术语“按值传递”和“按引用传递”是在谈论变量。按值传递意味着将变量的传递给函数/方法。按引用传递意味着将对该变量的引用传递给函数。后者为函数提供了一种更改变量内容的方式。
按照这些定义,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"。在函数foo中,Dog "Fifi"作为对象引用按值传递,因此main中的aDog值未更改。如果按引用传递,则在调用foo后,main中的aDog.getName()将返回"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(...)内设置的。任何food执行的操作都是针对实际上执行在aDog上的操作,但是不可能更改变量aDog本身的值。
有关按引用传递和按值传递的更多信息,请参阅以下答案:https://dev59.com/s3RC5IYBdhLWcg3wOOP1#430958。这更详细地解释了两者的语义和历史,并解释了为什么Java和许多其他现代语言在某些情况下似乎都执行这两种方式。

11
在第一个例子中,“Fifi”会发生什么?它会停止存在,从未被创建,还是在堆中存在但是没有栈中的引用变量? - dbrewster
97
对我来说,说一个对象的引用是按值传递与说这个对象是按引用传递是一样的。我是Java新手,但我认为(相比之下)原始数据类型是按值传递的。 - user36800
23
你错了。你是否仔细检查了与Fifi的示例并查看结果?请确认即使foo覆盖了d的值并显示所有输入到一个函数都是按值传递,但foo(aDog);确实没有改变aDog的值。 - user21820
17
两个声明都是错误的。通过引用传递对象意味着如果函数修改变量,则修改对象本身。这不是Java中发生的情况;对象不能通过引用进行传递,而只能将引用作为输入传递给函数,当函数执行d = new Dog("Fifi");时,它覆盖了输入变量d,该变量存储一个引用,但不是“通过引用传递的对象”。与C中函数签名中的&d相比,后者是按引用传递的。 - user21820
67
@dbrewster,很抱歉,但是“Fifi”已经不在我们中间了。 - ghilesZ
显示剩余37条评论

3573

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

Java规范指出,在Java中所有的内容都是按值传递的。在Java中并不存在所谓的“按引用传递”。

理解这一点的关键在于,例如:

Dog myDog;

它并不是一只狗,实际上它是指向一只狗的指针。在Java中使用“引用”这个术语非常具有误导性,这也是导致大部分混淆的原因。他们所谓的“引用”在大多数其他语言中更像是我们称之为“指针”的东西。

这意味着当你拥有

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(地址为42的Dog / code>对象)
    • 要求该Dog (地址为42)更改其名称为Max
  • 在“BBB”行
    • 创建一个新的Dog 。 假设他在地址74处
    • 我们将参数 someDog 分配给74
  • 在“CCC”行
    • 跟随someDog 指向的Dog(地址为74的Dog / code>对象)
    • 要求该Dog (地址为74)更改其名称为Rowlf
  • 然后,我们返回

现在让我们考虑方法外发生的事情:

我的狗有变化吗?

这是关键。

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

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

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

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

如果Java具有传递按引用语义,则我们上面定义的 foo 方法在分配 someDog 时会更改 myDog 指向的位置。

将引用参数视为传递的变量的别名。当分配该别名时,传递的变量也会被分配。

更新

在评论中的讨论需要一些澄清...

在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都在模拟传递引用。他们仍然传递值(指向整数或数组的指针),并在被调用的函数内部跟随这些指针来操作数据。
按引用传递全部关乎函数的声明/定义以及它如何处理其参数。引用语义适用于对该函数的每个调用,调用站点只需要传递变量,而不需要额外的数据结构。
这些模拟需要调用站点和函数进行合作。毫无疑问,这是有用的,但仍然是按值传递。

3
有两种类型的变量在Java中,即基本类型和引用类型。基本类型包括boolean,byte,char,short,int,long,float和double,而引用类型则指向对象。基本类型直接存储值,而引用类型存储对对象的引用。一些常见的引用类型包括String,Thread,File等。 - Ravikumar Rajendran
13
@Jonathan那个链接是关于C++的,不是C。C的工作方式严格遵循按值传递,就像Java一样。如果你传递一个指针,那么这个_指针_就是你可以跟随的值。你不能改变指针本身,但可以跟随它并更改它所指向的值。如果你重新指向它,调用者看不到这个变化。在C++中,你可以传递一个对某物的_引用_(在你引用的那个页面中被称为int&),它类似于一个别名;如果你在一个函数/方法中更改它,它确实会改变作为参数传递的对象/基元/指针。 - Scott Stanchfield
2
@Jonathan 这在Java中类似于这个:https://pastebin.com/1tZsVVRw。*正在创建指向参数的指针(它本身可能是指针),这类似于创建一个“桶”来保存值 - Java不允许创建C语法和指针以操作现有数据,但这并不意味着Java没有指针(请注意,C仍然没有引用语义...)。例如,Pascal以与C的*类似的方式使用^ - 语言具有不同的语法并不意味着它们没有相同的概念(如指针)。 - Scott Stanchfield
4
@Jonathan,C语言只支持按值传递(在你的示例中,你传递了指针的值-请参见https://dev59.com/VXE95IYBdhLWcg3wn_Vr,特别是Ely的答案)。在编译器术语中,“按引用传递”有非常具体的含义。顺便说一下,我是一名编译器开发人员,并且曾经参与过ANSI C ++ 98委员会的工作...C ++具有引用语义; C语言没有。区别在于实际参数是否可以被修改。当你传入&i时,实际参数值是i的地址,而不是对i的引用。 - Scott Stanchfield
2
@Amir 这些是错误的。C仅支持值参数。请参阅https://dev59.com/VXE95IYBdhLWcg3wn_Vr,了解一些好的解释,特别是关于“传递引用的错觉”的评论。 - Scott Stanchfield
显示剩余41条评论

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

    enter image description here

  2. 从方法的角度来看,声明了一个类型为Foo、名为a的引用,并将其初始赋值为null

    public static void changeReference(Foo a)
    

    enter image description here

  3. 当调用changeReference方法时,引用a将被赋值为作为参数传递的对象。

    changeReference(f);
    

    enter image description here

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

    Foo b = new Foo("b");
    

    enter image description here

  5. a = b对引用a进行了新的赋值,而不是f,它指向具有属性"b"的对象。

    enter image description here

  6. 当调用modifyReference(Foo c)方法时,创建了一个名为c的引用,并将其赋值为具有属性"f"的对象。

    enter image description here

  7. c.setAttribute("c");将更改引用c指向的对象的属性,该对象与引用f指向的对象相同。

    enter image description here


69
Java总是按值传递参数,但你传递的是对象引用的值,而不是对象的副本。简单明了吧? - dan carter
“Object not by Reference”,真的吗? - Sam Ginrich
4
这是我见过的解决Java参考问题最好的答案。非常感谢。 - Mohammad_Hosein

856

Java始终按值传递,没有任何例外。

那么为什么会有人对此感到困惑,并认为Java是按引用传递,或者认为他们有一个Java作为按引用传递的示例呢?关键在于Java从未在任何情况下直接提供对对象本身的值的访问。唯一访问对象的方式是通过对该对象的引用。因为Java对象总是通过引用而不是直接访问,所以当严谨地说时,将字段、变量和方法参数称为对象是很常见的,尽管它们实际上只是对象的引用混淆源于这种(严格来说是不正确的)术语变化。

因此,在调用方法时:

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

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

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


JVMS 2.2非常清楚地表明:有两种类型的值可以存储在变量中,作为参数传递,由方法返回并进行操作:原始值和引用值。对象引用是值。一切都是按值传递的。 - Brian Goetz
https://www.geeksforgeeks.org/g-fact-31-java-is-strictly-pass-by-value/ - georgiana_e
操作上的含义是:f(x)(传递一个变量)永远不会将值赋给x本身。不存在传递变量地址(别名)的情况。这是一种稳健的语言设计决策。 - Joop Eggen
所以基本上我们传递地址,并在我们的方法中引用该地址,例如在C语言中 int test(int *a) { int b = *(a); return b;) ? - bwass31
所以,当我想将一个对象传递给某个方法时,我注定要失败,因为对象不是“值”:( - Sam Ginrich

799

这将为您提供有关Java如何工作的一些见解,以至于在您下次讨论Java按引用传递还是按值传递时,您只会微笑 :-)

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

第二步,请记住,当您将一个对象传递到方法中时,您传递的是对象的引用,而不是对象本身。

  • 学生:大师,这是否意味着Java是按引用传递的?
  • 大师:弟子,不是。

现在想想一个对象的引用/变量是什么:

  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。
  • 持有指向对象地址的变量person在第3行被传递给函数。
  • 在第4行,你会听到寂静的声音
  • 检查第5行的注释
  • 创建了一个方法局部变量-anotherReferenceToTheSamePersonObject-然后在第6行发生了魔法:
    • 变量/引用person被逐位复制并传递到函数内的anotherReferenceToTheSamePersonObject
    • 没有创建新的Person实例。
    • person”和“anotherReferenceToTheSamePersonObject”都持有值为3bad086a的相同引用。
    • 不要尝试这样做,但person == anotherReferenceToTheSamePersonObject将为true。
    • 两个变量都具有引用的相同副本,它们都引用同一对象堆上的同一Person对象,而不是副本。

一张图片胜过千言万语:

Pass by Value

请注意,另一个指向同一人对象的引用箭头是指向对象而不是变量person!如果你没明白,那就相信我并记住最好说Java是按值传递。嗯,按引用值传递。哦,甚至更好的是通过复制变量值传递!现在随意讨厌我,但请注意,在谈论方法参数时,原始数据类型和对象之间没有区别。你总是传递引用值的位的副本!如果是原始数据类型,这些位将包含原始数据类型本身的值。如果是对象,则位将包含告诉JVM如何到达对象的地址的值。Java是按值传递的,因为在方法内部,你可以尽情修改所引用的对象,但无论你怎么努力,都无法修改传递的变量,它将继续引用(而不是p_______)相同的对象!
上面的changeName函数永远无法修改传递引用的实际内容(位值)。换句话说,changeName不能使Person person引用另一个对象。
当然,您可以简单地说 Java是按值传递的!

我尝试了这个:File file = new File("C:/"); changeFile(file); System.out.println(file.getAbsolutePath());}public static void changeFile(File f) { f = new File("D:/"); }` - Excessstone

392

Java传递的是值引用。

所以,您无法更改传递的引用。


这引出了一个问题,即Java是否是一种面向对象或参考定向的语言,而不仅仅是“传递参数的机制”。https://zh.wikipedia.org/wiki/Java - Sam Ginrich
多么美丽而简洁的解释。 - Muhammad Zahab

295

我觉得争论“按引用传递 vs 按值传递”不是特别有帮助。

如果你说,“Java 是按照什么传递的(引用/值)”,在任一情况下,你都没有提供完整的答案。下面提供一些额外信息,希望有助于理解在内存中发生的情况。

在我们讨论 Java 实现之前,先来了解一下堆栈/堆的基础知识: 值按规则地在堆栈上进出,就像自助餐厅的盘子堆一样有序。 堆内存(也称为动态内存)是混乱和无组织的。JVM 只是在能找到空间的地方分配内存,并在不再需要使用它的变量时释放它。

好的。首先,局部原始类型会被放在栈上。所以这段代码:

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

导致这个结果:

栈上的原语

当你声明并实例化一个对象时,实际的对象被存储在堆内存中。那么栈上存储了什么呢?是指向堆内存中对象的地址。C++程序员称之为指针,但一些Java开发人员反对使用“指针”这个词。无论如何,知道对象的地址存储在栈上即可。

就像这样:

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

数组是一个对象,因此它也存储在堆上。那么,数组中的对象呢?它们会获得自己的堆空间,并且每个对象的地址都会放在数组内。

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);
}
一串字符串被创建并在堆中分配了空间,该字符串的地址被存储在栈中并赋予标识符“hisName”,由于第二个字符串的地址与第一个相同,因此不会创建新字符串,也不会分配新的堆空间,但是在栈上创建了一个新的标识符。然后我们调用 shout():创建一个新的堆栈帧并创建一个新标识符 name,并将其分配给已经存在的字符串的地址。

la da di da da da da

所以,值,引用?你说“土豆”。

12
这个回答好极了,即使像我这样的傻瓜也能理解。我还要补充并更正一下,“按值传递”字面上意思是将栈中的文字数值传递。 - Dude156
太可爱了,是最佳答案。 - Mehdi Monzavi
准确地说,当你想要说“通过引用传递了一个对象”时,战争就开始了。 - Sam Ginrich
第一段感到鼓舞。所有置顶答案都坚守严格、狭隘的按值/按引用传递定义,而没有考虑程序员实际上感兴趣的内容。 - fishinear

235

基本上,重新分配对象参数不会影响参数本身,例如:

private static 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!"的引用。如果它是引用本身,那么foo会重新定义baznull


219

仅为对比展示以下 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 只有两种传递方式:对于内置类型采用值传递,而对于对象类型采用指针的值传递。


1
这表明Java不是按值传递的,因为它不像C++那样将整个对象复制到堆栈上,就像上面的例子中所示-...,Dog obj,... - Solubris
3
不,Java是按值传递引用的。这就是为什么在Java示例中重写objPtr时,原始的Dog对象不会改变。但是如果修改objPtr所指向的对象,则会改变原始对象。 - Eclipse

211

Java以传值方式传递对象引用。


6
没有用的解释。 - Johnes
你的意思是Java通过将值复制到引用来传递。 - skystar7

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接