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

Posted

技术标签:

【中文标题】Java 是“按引用传递”还是“按值传递”?【英文标题】:Is Java "pass-by-reference" or "pass-by-value"? 【发布时间】:2016-11-28 18:59:00 【问题描述】:

我一直认为 Java 使用 pass-by-reference

但是,我看到 a blog post 声称 Java 使用 pass-by-value

我想我不明白他们的区别。

解释是什么?

【问题讨论】:

我们更常说“通过引用”的变量可以被变异。该术语出现在教科书中是因为语言理论家需要一种方法来区分您如何处理原始数据类型(int、bool、byte)和复杂和结构化对象(数组、流、类)——也就是说,那些可能是无限内存的分配。 我想指出,在大多数情况下您不必考虑这一点。我编程 java 很多年,直到我学会了 c++。在此之前,我不知道什么是按引用传递和按值传递。直观的解决方案总是对我有用,这就是为什么 java 是最适合初学者的语言之一。因此,如果您目前担心,如果您的函数需要引用或值,只需按原样传递即可。 Java 按值传递引用。 简而言之,之所以会出现这种混淆,是因为在 Java 中,所有非原始数据类型都由 references 处理/访问。然而,传递总是有价值的。因此,对于所有非原始类型,引用都是按其值传递的。所有原始类型也是按值传递的。 我觉得这很有帮助:baeldung.com/java-pass-by-value-or-pass-by-reference 【参考方案1】:

Java 总是按值传递。不幸的是,当我们处理对象时,我们实际上是在处理称为 references 的对象句柄,它们也是按值传递的。这种术语和语义很容易让许多初学者感到困惑。

是这样的:

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 不会在函数 fooDog "Fifi" 中更改,因为对象引用是按值传递的。如果它是通过引用传递的,那么main 中的aDog.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(...) 中设置的。 food 执行的任何操作,实际上都是在aDog 上执行的,但是不可能更改变量aDog 的值自己。

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

【讨论】:

那么在第一个示例中“Fifi”会发生什么?它是不复存在、从未创建过,还是存在于堆中但堆栈中没有引用变量? 对我来说,说对象的引用是按值传递的,就等于说对象是按引用传递的。我是 Java 新手,但我认为(相比之下)原始数据 是按值传递的。 @user36800:你错了。您是否与 Fifi 一起完成了示例并仔细查看了结果?检查foo(aDog); 确实没有改变aDog 尽管foo 覆盖了d 的值,这表明函数的所有输入确实都是按值传递的。 @user36800:嗯,这两种说法都是错误的。通过引用传递对象意味着如果函数修改变量,那么它会修改对象本身。这不是Java中发生的事情。对象不能通过引用传递,而是只能将引用作为输入传递给函数,并且当函数执行d = new Dog("Fifi"); 时,它会覆盖输入变量d,该变量存储一个引用但不是“通过引用传递的对象”。与 C 中函数签名中的 &d 形成对比,这将是按引用传递。 [续] @dbrewster 很抱歉,但是……“Fifi”已经不在我们中间了【参考方案2】:

我刚刚注意到你引用了my article。

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

理解这一点的关键是像

Dog myDog;

不是狗;它实际上是一个指向 Dog 的 指针。在 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 对象) Dog(地址为 42 的那个)被要求将他的名字改为 Max 在“BBB”行 创建了一个新的Dog。假设他在地址 74 我们将参数someDog分配给74 在“CCC”行 someDog 后面跟着它指向的Dog(地址为74 的Dog 对象) Dog(地址为 74 的那个)被要求改名为 Rowlf 然后,我们返回

现在让我们想想方法之外发生了什么:

myDog 改变了吗?

有钥匙。

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

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

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

在 C++、Ada、Pascal 等支持 pass-by-reference 的语言中,实际上可以更改被传递的变量。

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

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

【讨论】:

对上述示例的小澄清问题,所以当在地址 72 的 BBB 处创建新 Dog 时,这是否意味着返回创建的 Dog 在 72 处并且它的值丢失并恢复为 42?跨度> @ebresie javarevisited.blogspot.com/2015/09/…. @ebresie 大部分是的(我稍后会澄清“大部分”)。指向 74 处的新狗的唯一指针(我假设您的意思是 74 而不是 72)是 foo 函数的参数。当 foo 返回时,它的所有参数都从堆栈中弹出,所以没有任何东西指向 72,它可以被垃圾回收。我说“大部分”是因为没有“恢复”发生;调用者中的指针 myDog 一直指向 42 并且永远不会改变,无论函数中发生了什么,因此,没有“revert”。 @NiharGht 好点 - 我已经澄清了(如果仍然不清楚,请再次评论) Java 不像 C调用的范围。从语言中寻找这种行为是 const 关键字的目的。请停止说 java 就像 C .见:courses.washington.edu/css342/zander/css332/passby.html【参考方案3】:

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


让我通过example 解释一下:

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


我将分步解释:

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

    Foo f = new Foo("f");
    

    在方法方面,声明了一个名为 a 的类型为 Foo 的引用,并且最初分配了 null

    public static void changeReference(Foo a)
    

    当您调用方法changeReference 时,引用a 将被分配作为参数传递的对象。

    changeReference(f);
    

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

    Foo b = new Foo("b");
    

    a = b 对属性为 "b" 的对象的引用 anot f 进行新分配。

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

    c.setAttribute("c");会改变引用c指向它的对象的属性,和引用f指向它的对象是同一个对象。

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

【讨论】:

Java 总是按值传递参数,但按值传递的是对对象的引用,而不是对象的副本。简单吧? 我希望我学习 Java 时所用的 Herbert Schildt 的书教过这个 “对象不是引用”,真的吗?【参考方案4】:

Java 总是按值传递,没有例外,永远

那么,为什么任何人都会对此感到困惑,并认为 Java 是按引用传递的,或者认为他们有 Java 充当按引用传递的示例?关键点是,在任何情况下,Java从不提供对对象本身的值的直接访问。对对象的唯一访问是通过对该对象的reference。因为 Java 对象总是通过引用而不是直接访问,所以通常将字段和变量以及方法参数称为对象 ,当迂腐的时候,它们只是对对象的引用混淆源于这种(严格地说,不正确的)命名变化。

所以,当调用方法时

对于原始参数(intlong 等),传递值是原始参数的实际值(例如,3)。 对于对象,传递值是对对象的引用的值。

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

自然地,通过值传递对对象的引用看起来非常像(实际上与通过引用传递对象没有区别)。

【讨论】:

JVMS 2.2 非常清楚地说明了这一点:有两种类型的值可以存储在变量中、作为参数传递、由方法返回以及对其进行操作:原始值 i> 和引用值。”对象引用是值。一切都是按值传递的。 geeksforgeeks.org/g-fact-31-java-is-strictly-pass-by-value 操作含义:f(x)(传递变量)永远不会分配给x 本身。没有传递变量地址(alias)之类的东西。 一个可靠的语言设计决策。 所以基本上我们正在传递地址并在我们的方法中引用该地址,例如在 c int test(int *a) int b = *(a); return b;) 中? 所以,当我想将一个对象传递给某个方法时,我注定要失败,因为一个对象“不是一个值”:(【参考方案5】:

这将使您深入了解 Java 的真正工作原理,以至于在您下次讨论 Java 通过引用传递或按值传递时,您会微笑:-)

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

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

同学:老师,这是否意味着Java是通过引用传递的? 大师:蚱蜢,号

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

    变量保存一些位,这些位告诉 JVM 如何访问内存(堆)中的引用对象。 向方法传递参数时您传递的不是引用变量,而是引用变量中位的副本。像这样的东西:3bad086a。 3bad086a 表示获取传递对象的一种方式。 所以你只是传递 3bad086a 它是引用的值。 您传递的是引用的值,而不是引用本身(而不是对象)。 这个值实际上是被复制并赋予方法

在下面(请不要尝试编译/执行这个...):

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 会是真的。 两个变量都具有相同的引用副本,并且它们都引用同一个 Person 对象,即堆上的相同对象而不是副本。

一张图抵千言:

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

如果您不明白,那么请相信我并记住,最好说Java 是按价值传递的。好吧,按参考值传递。哦,更好的是 pass-by-copy-of-the-variable-value! ;)

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

您总是传递引用值的位的副本!

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

Java 是按值传递的,因为在方法内部,您可以根据需要修改引用的对象,但无论您多么努力,您永远无法修改将继续引用的传递变量(不是 p _ _ _ _ _ _ _) 无论如何都是同一个对象!


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


当然你可以简短地说 Java 是按值传递的!

【讨论】:

我试过这个: File file = new File("C:/");更改文件(文件); System.out.println(file.getAbsolutePath()); public static void changeFile(File f) f = new File("D:/"); `【参考方案6】:

Java 按值传递引用。

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

【讨论】:

【参考方案7】:

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

如果您说“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,并分配已经存在的String的地址。

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

【讨论】:

这样一个很棒的答案,即使是像我这样的傻瓜也能理解。我还要补充一下,“按值传递”字面意思是堆栈中的文字值被传递。 太可爱了,最好的答案 准确地说,战争开始了,当你想说“一个对象通过引用传递”时【参考方案8】:

只是为了显示对比,比较以下C++和Java sn-ps:

在 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 不是按值传递的,因为它不像 C++ 那样将整个对象复制到堆栈上,如上例所示 - ..., Dog obj,... 不,Java 按值传递引用。这就是为什么当您在 java 示例中覆盖 objPtr 时,原始 Dog 对象不会改变。但是如果修改 objPtr 指向的对象,它会这样做。【参考方案9】:

基本上,重新分配 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 值的副本,它只是对"Hah!" 的引用。如果它是实际引用本身,那么 foo 将重新定义 baznull

【讨论】:

【参考方案10】:

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

【讨论】:

不是一个有用的解释。【参考方案11】:

我不敢相信还没有人提到芭芭拉·利斯科夫。当她在 1974 年设计 CLU 时,她遇到了同样的术语问题,她发明了术语 call bysharing(也称为 call by object-sharing对象调用)针对这种“值是引用的值调用”的特定情况。

【讨论】:

:) 另一个术语,在 Java 岛引起了混乱,只是因为说“根据我们在堆栈中找到的对象是通过引用传递的”在政治上是不正确的。【参考方案12】:

问题的症结在于“通过引用传递”表达式中的reference这个词的含义与Java中reference这个词的通常含义完全不同。

通常在 Java 中 reference 表示 对对象的引用。但是编程语言理论中的技术术语按引用/值传递是指对保存变量的内存单元的引用,这是完全不同的。

【讨论】:

是的,对象引用在技术上是一个句柄,还不是地址,因此比“按值”更进一步。【参考方案13】:

在java中一切都是参考,所以当你有类似的东西时: Point pnt1 = new Point(0,0);Java 执行以下操作:

    创建新的 Point 对象 创建新的 Point 引用并将该引用初始化为对先前创建的 Point 对象的 point(引用)。 从这里开始,通过Point object life,您将通过pnt1 访问该对象 参考。所以我们可以说,在 Java 中,您通过对象的引用来操作对象。

Java 不通过引用传递方法参数;它通过值传递它们。我将使用来自this site的示例:

public static void tricky(Point arg1, Point arg2) 
  arg1.x = 100;
  arg1.y = 100;
  Point temp = arg1;
  arg1 = arg2;
  arg2 = temp;

public static void main(String [] args) 
  Point pnt1 = new Point(0,0);
  Point pnt2 = new Point(0,0);
  System.out.println("X1: " + pnt1.x + " Y1: " +pnt1.y); 
  System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);
  System.out.println(" ");
  tricky(pnt1,pnt2);
  System.out.println("X1: " + pnt1.x + " Y1:" + pnt1.y); 
  System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);  

程序流程:

Point pnt1 = new Point(0,0);
Point pnt2 = new Point(0,0);

创建两个不同的 Point 对象,并关联两个不同的引用。

System.out.println("X1: " + pnt1.x + " Y1: " +pnt1.y); 
System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);
System.out.println(" ");

预期的输出将是:

X1: 0     Y1: 0
X2: 0     Y2: 0

在这一行中,'pass-by-value' 发挥作用...

tricky(pnt1,pnt2);           public void tricky(Point arg1, Point arg2);

引用 pnt1pnt2 按值传递 到棘手的方法,这意味着现在您的引用 pnt1pnt2 将它们的 copies 命名为 arg1arg2.So pnt1arg1 指向同一个对象。 (pnt2arg2 相同)

tricky 方法中:

 arg1.x = 100;
 arg1.y = 100;

tricky 方法中的下一步

Point temp = arg1;
arg1 = arg2;
arg2 = temp;

在这里,您首先创建新的temp 点引用,它将指向arg1 引用一样的位置。然后将引用 arg1 移动到 point 到与 arg2 引用相同的位置。 最后arg2指向 到同一个地方,如temp

从这里tricky 方法的范围消失了,您无法再访问引用:arg1arg2temp但重要的是,当这些引用“在生活中”时,您对它们所做的一切都会永久影响它们指向的对象。

所以在执行tricky方法后,当你返回main时,就会出现这种情况:

所以现在,程序的完全执行将是:

X1: 0         Y1: 0
X2: 0         Y2: 0
X1: 100       Y1: 100
X2: 0         Y2: 0

【讨论】:

租金的一半:“一切”在您的帖子中都是“对象”。【参考方案14】:

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

首先我们要明白什么是值传递和引用传递。

按值传递意味着您正在内存中复制传入的实际参数的值。这是实际参数内容的副本

按引用传递(也称为按地址传递)是指存储实际参数的地址的副本

有时 Java 会产生按引用传递的错觉。让我们通过下面的例子来看看它是如何工作的:

public class PassByValue 
    public static void main(String[] args) 
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeValue(t);
        System.out.println(t.name);
    
    
    public void changeValue(Test f) 
        f.name = "changevalue";
    


class Test 
    String name;

这个程序的输出是:

changevalue

让我们一步一步理解:

Test t = new Test();

众所周知,它将在堆中创建一个对象并将引用值返回给 t。例如,假设t的值为0x100234(我们不知道实际的JVM内部值,这只是一个例子)。

new PassByValue().changeValue(t);

将引用 t 传递给函数时,它不会直接传递对象 test 的实际引用值,而是会创建 t 的副本,然后将其传递给函数。因为它是按值传递,它传递变量的副本而不是它的实际引用。由于我们说 t 的值为0x100234,因此 t 和 f 将具有相同的值,因此它们将指向同一个对象。

如果您使用引用 f 更改函数中的任何内容,它将修改对象的现有内容。这就是我们得到输出changevalue 的原因,它在函数中更新。

为了更清楚地理解这一点,请考虑以下示例:

public class PassByValue 
    public static void main(String[] args) 
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeRefence(t);
        System.out.println(t.name);
    
    
    public void changeRefence(Test f) 
        f = null;
    


class Test 
    String name;

这会抛出NullPointerException吗?不,因为它只传递引用的副本。 在通过引用传递的情况下,它可能会抛出一个NullPointerException,如下所示:

希望这会有所帮助。

【讨论】:

【参考方案15】:

已经有很好的答案涵盖了这一点。我想通过分享一个非常简单的示例(将编译)来对比 C++ 中的引用传递和 Java 中的值传递之间的行为,从而做出一点贡献。

几点:

    术语“引用”是一个具有两个不同含义的重载。在 Java 中,它仅表示一个指针,但在“按引用传递”的上下文中,它表示传入的原始变量的句柄。 Java 是按值传递。 Java 是 C(以及其他语言)的后代。在 C 之前,一些(但不是全部)早期语言,如 FORTRAN 和 COBOL 支持 PBR,但 C 不支持。 PBR 允许这些其他语言对子例程中传递的变量进行更改。为了完成同样的事情(即更改函数内部变量的值),C 程序员将指向变量的指针传递给函数。受 C 启发的语言,例如 Java,借用了这个想法,并继续像 C 一样将指针传递给方法,只是 Java 将其指针称为 References。同样,“引用”一词的用法与“通过引用”中的用法不同。 C++ 允许通过引用传递,方法是使用“&”字符声明引用参数(在 C 和 C++ 中,这恰好是用于指示“变量地址”的相同字符)。例如,如果我们通过引用传入一个指针,那么形参和实参不只是指向同一个对象。相反,它们是相同的变量。如果一个被设置为不同的地址或为空,另一个也会被设置。 在下面的 C++ 示例中,我将 指针 传递给 通过引用 以空值结尾的字符串。在下面的 Java 示例中,我通过值传递了对字符串的 Java 引用(同样,与指向字符串的指针相同)。注意 cmets 中的输出。

C++ 引用传递示例:

using namespace std;
#include <iostream>

void change (char *&str)   // the '&' makes this a reference parameter
    str = NULL;


int main()

    char *str = "not Null";
    change(str);
    cout<<"str is " << str;      // ==>str is <null>

Java 通过值传递“Java 引用”示例

public class ValueDemo

    public void change (String str)
        str = null;
    

     public static void main(String []args)
        ValueDemo vd = new ValueDemo();
        String str = "not null";
        vd.change(str);
        System.out.println("str is " + str);    // ==> str is not null!!
                                                // Note that if "str" was
                                                // passed-by-reference, it
                                                // WOULD BE NULL after the
                                                // call to change().
     

编辑

有几个人编写了 cmets,这似乎表明他们要么没有看我的示例,要么没有得到 c++ 示例。不确定断开连接在哪里,但猜测 c++ 示例不清楚。我在 pascal 中发布了相同的示例,因为我认为通过引用在 pascal 中看起来更清晰,但我可能是错的。我可能只会让人们更加困惑;我希望不会。

在 pascal 中,通过引用传递的参数称为“var 参数”。在下面的过程 setToNil 中,请注意参数“ptr”之前的关键字“var”。当一个指针被传递给这个过程时,它将被通过引用传递。注意行为:当这个过程将 ptr 设置为 nil 时(这是 pascal 对 NULL 的说法),它会将参数设置为 nil——你不能在 Java 中这样做。

program passByRefDemo;
type 
   iptr = ^integer;
var
   ptr: iptr;

   procedure setToNil(var ptr : iptr);
   begin
       ptr := nil;
   end;

begin
   new(ptr);
   ptr^ := 10;
   setToNil(ptr);
   if (ptr = nil) then
       writeln('ptr seems to be nil');      ptr should be nil, so this line will run. 
end.

编辑 2

Ken Arnold、James Gosling(Java 的发明者) 和 David Holmes 的 “THE Java 编程语言” 的部分摘录,第 2 章第 2.6 节。 5

方法的所有参数都“按值”传递。换一种说法, 方法中参数变量的值是调用者的副本 指定为参数。

他继续对物体提出同样的观点。 . .

你应该注意,当参数是一个对象引用时,它是 “按值”传递的对象引用(而不是对象本身)

在同一部分的结尾,他更广泛地声明了 java 仅通过值传递,从不通过引用传递。

Java 编程语言不通过引用传递对象;它 按值传递对象引用。因为两个相同的副本 引用指的是同一个实际对象,通过一个所做的更改 引用变量通过另一个可见。正好有一个 参数传递模式-按值传递-这有助于保持事物 很简单。

本书的这一部分对 Java 中的参数传递以及按引用传递和按值传递之间的区别进行了很好的解释,它是由 Java 的创建者编写的。我会鼓励任何人阅读它,尤其是如果你仍然不相信的话。

我认为这两个模型之间的差异非常微妙,除非您在实际使用过引用传递的地方完成了编程,否则很容易忽略两个模型的不同之处。

我希望这能解决争论,但可能不会。

编辑 3

我可能对这篇文章有点着迷。可能是因为我觉得 Java 的制造者无意中传播了错误信息。如果他们没有使用“引用”这个词来表示指针,而是使用了其他东西,比如说 丁香果,不会有问题的。你可以说,“Java 通过值而不是通过引用传递 dingleberries”,没有人会感到困惑。

这就是只有 Java 开发人员对此有异议的原因。他们看着“参考”这个词,并认为他们确切地知道这意味着什么,所以他们甚至懒得考虑相反的论点。

无论如何,我注意到一篇较早的帖子中的评论,它做了一个我非常喜欢的气球类比。如此之多,以至于我决定将一些剪贴画粘在一起,制作一组卡通来说明这一点。

按值传递引用——对引用的更改不会反映在调用者的范围内,但对对象的更改会。这是因为引用被复制,但原始和副本都引用同一个对象。

通过引用传递--没有引用的副本。调用者和被调用的函数共享单一引用。对引用或对象数据的任何更改都会反映在调用者的范围内。

编辑 4

我看过关于这个主题的帖子,其中描述了 Java 中参数传递的低级实现,我认为这很棒而且非常有帮助,因为它使抽象的想法具体化。但是,对我来说,问题更多的是关于语言规范中描述的行为,而不是关于行为的技术实现。这是Java Language Specification, section 8.4.1 的摘录:

当调用方法或构造函数时(第 15.12 节), 实际参数表达式初始化新创建的参数 变量,每个声明的类型,在执行主体之前 方法或构造函数。 出现在 DeclaratorId 可以用作方法主体中的简单名称或 构造函数来引用形参。

这意味着,java 在执行方法之前创建传递参数的副本。像大多数在大学学习编译器的人一样,我使用了"The Dragon Book",这是THE编译器书籍。第 1 章中对“Call-by-value”和“Call-by-Reference”有很好的描述。Call-by-value 描述与 Java Specs 完全匹配。

当我在 90 年代学习编译器时,我使用了 1986 年的第一版,它比 ​​Java 早了大约 9 或 10 年。但是,我刚刚遇到了 2007 年的 2nd Eddition 的副本实际上提到了 Java! 标记为“参数传递机制”的第 1.6.6 节很好地描述了参数传递。这是标题“按值调用”下的摘录,其中提到了 Java:

在按值调用中,评估实际参数(如果它是 表达式)或复制(如果它是一个变量)。值被放置在 对应的形参的位置 称为程序。 该方法用于C和Java中,是常用的 C++ 以及大多数其他语言中的选项。

【讨论】:

“Java 编程语言不通过引用传递对象;它通过值传递对象引用。”我认为这种说法整体上是错误的。假设,您想将一个对象传递给一个方法... @SamGinrich,在这种情况下,您传递的是对该对象的引用。该对象存在于内存中的某处。引用(也称为指针)类似于保存对象内存地址的原语(如 long)。传递到方法中的实际上是引用的副本。由于您正在传递引用的副本,因此这是按值传递(即您正在按值传递引用)。如果您在方法内将副本设置为 null,则不会影响原始副本。如果这是通过引用传递,则将副本设置为 null 也会将原始设置为 nul @SamGinrich 看看我的代码示例和我发布的两个图表。 完全同意您的介绍。我怀疑 Ken Arnold 等人。有资格重命名长期建立的概念,其中“传递对象引用”等同于“通过引用传递对象”,因为您显然在共享对象上工作,并且为此目的的引用是不变的,类似于指针。我在大学就是这样学的。现在,在用例“将对象传递给某个方法”中,关注引用的技术方面似乎是人为缩小,而不是追赶:“被调用的方法是在原始对象上工作还是在副本上工作?” 哎呀,我认为我完全同意你上面的回答,虽然没有引用定义,这既不是来自你也不是来自我。【参考方案16】:

Java 是按值传递(堆栈内存)

它是如何工作的

我们先了解一下java存储原始数据类型和对象数据类型的地方。

原始数据类型本身和对象引用存储在堆栈中。 对象本身存储在堆中。

这意味着,堆栈内存存储原始数据类型以及 对象的地址。

而且你总是传递引用值的位的副本。

如果是原始数据类型,那么这些复制的位包含原始数据类型本身的值,这就是为什么当我们在方法内部更改参数的值时,它不会反映外部的更改。

如果它是像 Foo foo=new Foo() 这样的对象数据类型,那么在这种情况下,对象地址的副本会像文件快捷方式一样传递,假设我们有一个文本文件 abc .txtC:\desktop 并假设我们制作同一个文件的快捷方式并将其放在 C:\desktop\abc-shortcut 中,这样当您访问C:\desktop\abc.txt 中的文件并写入 'Stack Overflow' 并关闭文件,然后再次从快捷方式打开文件,然后写入 '是供程序员学习的最大在线社区',那么总文件更改将是 'Stack Overflow 是供程序员学习的最大在线社区',这意味着无论您来自哪里都无关紧要打开文件,每次我们访问同一个文件,这里我们可以假设 Foo 为一个文件,假设 foo 存储在 123hd7h(原始地址如 C: \desktop\abc.txt ) 添加ress 和 234jdid(复制的地址如 C:\desktop\abc-shortcut 实际上包含里面文件的原始地址).. 所以为了更好的理解制作快捷方式文件和感觉。

【讨论】:

“Java 是一种编程语言”怎么样? “Java 的设计者构建了他们自己的术语,这在外部是不存在的”怎么样?【参考方案17】:

无论您使用什么语言,引用始终是一个值。

跳出框框视图,让我们看看汇编或一些低级内存管理。在 CPU 级别,如果将任何内容写入内存或 CPU 寄存器之一,则对任何内容的 reference 都会立即变为 value。 (这就是为什么 pointer 是一个很好的定义。它是一个值,同时具有目的)。

内存中的数据有一个位置,并且在那个位置有一个值(字节,字,等等)。在汇编中,我们有一个方便的解决方案,可以为某个 Location(又名变量)赋予一个 Name,但是在编译代码时,汇编器只是简单地替换 Name > 指定位置就像你的浏览器用IP地址替换域名一样。

从技术上讲,在不表示任何语言的情况下(当它立即成为一个值时)传递对任何语言的任何内容的引用是不可能的。

假设我们有一个变量 Foo,它的 Location 在内存中的第 47 个字节,它的 Value 是 5。我们还有另一个变量 Ref2Foo 位于内存中的第 223 个字节,其值为 47。此 Ref2Foo 可能是一个技术变量,不是由程序显式创建的。如果您只看 5 和 47 而没有任何其他信息,您只会看到两个 Values。 如果您将它们用作参考,那么要联系5,我们必须旅行:

(Name)[Location] -> [Value at the Location]
---------------------
(Ref2Foo)[223]  -> 47
(Foo)[47]       -> 5

这就是跳转表的工作方式。

如果我们想用 Foo 的值调用方法/函数/过程,有几种可能的方法可以将变量传递给方法,具体取决于 语言 及其几种方法调用模式:

    5 被复制到 CPU 寄存器之一(即 EAX)。 5 被推入堆栈。 47 被复制到 CPU 寄存器之一 47 推送到堆栈。 223 被复制到 CPU 寄存器之一。 223 被推送到堆栈。

在任何高于一个值的情况下——一个现有值的副本——已经被创建,现在由接收方法来处理它。当您在方法中写入“Foo”时,它要么从 EAX 中读出,要么自动取消引用,或双重取消引用,该过程取决于语言的工作方式和/或 Foo 的类型规定.这对开发人员是隐藏的,直到她绕过取消引用过程。因此,reference 在表示时是 value,因为引用是必须处理的值(在语言级别)。

现在我们已经将 Foo 传递给了方法:

在第 1 和第 2 种情况下,如果您更改 Foo (Foo = 9),它只会影响本地范围,因为您拥有该值的副本。从方法内部,我们甚至无法确定原始 Foo 在内存中的位置。 在情况 3. 和 4. 如果您使用默认语言结构并更改 Foo (Foo = 11),它可能会全局更改 Foo(取决于语言,即 Java 或类似 Pascal 的 procedure findMin(x, y, z: integer;var m : integer);)。但是,如果该语言允许您绕过取消引用过程,您可以更改47,例如更改为49。如果您阅读它,此时 Foo 似乎已更改,因为您已更改 本地指针 指向它。如果您要在方法 (Foo = 12) 中修改此 Foo,您可能会 FUBAR 程序的执行(又名 segfault),因为您将写入与预期不同的内存,您甚至可以修改一个注定的区域保存可执行程序并向其写入将修改运行代码(Foo 现在不在47)。但是 Foo 的 47 的值没有全局变化,只有方法内部的那个,因为 47 也是方法的副本。 在情况 5. 和 6. 中,如果您在方法内部修改 223,它会产生与 3. 或 4. 中相同的混乱(一个指针,指向现在错误的值,再次用作指针)但这仍然是一个本地问题,因为 223 已复制。但是,如果您能够取消引用Ref2Foo(即223),达到并修改指向值47,例如,修改为49,它将影响Foo 全局,因为在这种情况下,这些方法获得了 223 的副本,但引用的 47 仅存在一次,将其更改为 49 将导致每个 Ref2Foo 双重取消引用到错误的值。

挑剔无关紧要的细节,即使是通过引用传递的语言也会将值传递给函数,但这些函数知道它们必须使用它来解除引用。这个 pass-the-reference-as-value 只是对程序员隐藏了,因为它实际上没有用,而且术语只是 pass-by-reference

Strict pass-by-value 也是没用的,这意味着每次调用以数组为参数的方法时都必须复制一个 100 MB 的数组,因此 Java 不能严格值传递。每种语言都会传递对这个巨大数组的引用(作为值),并且如果该数组可以在方法内部本地更改,或者允许方法(如 Java)全局修改数组(从调用者的观点)和一些语言允许修改引用本身的值。

简而言之,用 Java 自己的术语来说,Java 是 pass-by-value 其中 value 可以是:real value,表示引用

【讨论】:

【参考方案18】:

据我所知,Java 只知道按值调用。这意味着对于原始数据类型,您将使用副本,对于对象,您将使用对对象的引用的副本。但是我认为有一些陷阱;例如,这将不起作用:

public static void swap(StringBuffer s1, StringBuffer s2) 
    StringBuffer temp = s1;
    s1 = s2;
    s2 = temp;



public static void main(String[] args) 
    StringBuffer s1 = new StringBuffer("Hello");
    StringBuffer s2 = new StringBuffer("World");
    swap(s1, s2);
    System.out.println(s1);
    System.out.println(s2);

这将填充 Hello World 而不是 World Hello,因为在交换函数中,您使用对主引用没有影响的副本。但是如果你的对象不是不可变的,你可以改变它,例如:

public static void appendWorld(StringBuffer s1) 
    s1.append(" World");


public static void main(String[] args) 
    StringBuffer s = new StringBuffer("Hello");
    appendWorld(s);
    System.out.println(s);

这将在命令行上填充 Hello World。如果您将 StringBuffer 更改为 String 它只会产生 Hello,因为 String 是不可变的。例如:

public static void appendWorld(String s)
    s = s+" World";


public static void main(String[] args) 
    String s = new String("Hello");
    appendWorld(s);
    System.out.println(s);

但是,您可以像这样为 String 制作一个包装器,使其能够与 Strings 一起使用:

class StringWrapper 
    public String value;

    public StringWrapper(String value) 
        this.value = value;
    


public static void appendWorld(StringWrapper s)
    s.value = s.value +" World";


public static void main(String[] args) 
    StringWrapper s = new StringWrapper("Hello");
    appendWorld(s);
    System.out.println(s.value);

编辑:我相信这也是在“添加”两个字符串时使用 StringBuffer 的原因,因为您可以修改原始对象,而使用 String 这样的不可变对象则无法修改。

【讨论】:

【参考方案19】:

在Java中,方法参数都是按值传递的:

Java 参数都是按值传递的(方法使用时会复制值或引用):

对于原始类型,Java 行为很简单: 该值被复制到原始类型的另一个实例中。

在对象的情况下,这是相同的: 对象变量是使用“new”关键字创建的引用(内存桶只保存对象的地址而不是原始值),并且像原始类型一样被复制。

行为可能与原始类型不同:因为复制的对象变量包含相同的地址(指向相同的对象)。 Object 的 content/members 可能仍会在方法内进行修改,然后在外部进行访问,从而产生(包含)Object 本身是通过引用传递的错觉。

“字符串”对象似乎是一个很好的反例,可以与“对象通过引用传递”的都市传说相提并论:

实际上,使用一种方法,您将永远无法更新作为参数传递的字符串的值:

一个字符串对象,通过一个声明为 final 的数组来保存字符,该数组不能被修改。 只有对象的地址可能被另一个使用“new”替换。 使用“new”更新变量,不会让外部访问对象,因为变量最初是按值传递并复制的。

【讨论】:

【参考方案20】:

不,它不是通过引用传递的。

Java 是根据 Java 语言规范按值传递的:

当调用方法或构造函数时(第 15.12 节),实际参数表达式的值会在方法体执行之前初始化新创建的参数变量,每个声明的类型或构造函数。出现在 DeclaratorId 中的 Identifier 可以用作方法或构造函数的主体中的简单名称,以引用 formal parameter。

【讨论】:

【参考方案21】:

让我试着用四个例子来解释我的理解。 Java 是按值传递,而不是按引用传递

/**

按值传递

在 Java 中,所有参数都是按值传递的,即分配方法参数对调用者是不可见的。

*/

示例 1:

public class PassByValueString 
    public static void main(String[] args) 
        new PassByValueString().caller();
    

    public void caller() 
        String value = "Nikhil";
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    

    public String method(String value, boolean valueflag) 
        value = "Anand";
        valueflag = true;
        return "output";
    

结果

output : output
value : Nikhil
valueflag : false

示例 2:

/** * * 按值传递 * */

public class PassByValueNewString 
    public static void main(String[] args) 
        new PassByValueNewString().caller();
    

    public void caller() 
        String value = new String("Nikhil");
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    

    public String method(String value, boolean valueflag) 
        value = "Anand";
        valueflag = true;
        return "output";
    

结果

output : output
value : Nikhil
valueflag : false

示例 3:

/** 这个'Pass By Value'有种'Pass By Reference'的感觉

有人说原始类型和'String'是'按值传递' 并且对象是“通过引用传递”。

但是从这个例子中,我们可以理解它实际上只是传值, 请记住,这里我们将引用作为值传递。 即:引用是按值传递的。 这就是为什么能够改变并且在本地范围之后仍然适用的原因。 但是我们不能在原始范围之外更改实际引用。 下一个 PassByValueObjectCase2 示例演示了这意味着什么。

*/

public class PassByValueObjectCase1 

    private class Student 
        int id;
        String name;
        public Student() 
        
        public Student(int id, String name) 
            super();
            this.id = id;
            this.name = name;
        
        public int getId() 
            return id;
        
        public void setId(int id) 
            this.id = id;
        
        public String getName() 
            return name;
        
        public void setName(String name) 
            this.name = name;
        
        @Override
        public String toString() 
            return "Student [id=" + id + ", name=" + name + "]";
        
    

    public static void main(String[] args) 
        new PassByValueObjectCase1().caller();
    

    public void caller() 
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student);
    

    public String method(Student student) 
        student.setName("Anand");
        return "output";
    

结果

output : output
student : Student [id=10, name=Anand]

示例 4:

/**

除了 Example3 (PassByValueObjectCase1.java) 中提到的之外,我们不能在原始范围之外更改实际引用。”

注意:我没有粘贴private class Student 的代码。 Student 的类定义与 Example3 相同。

*/

public class PassByValueObjectCase2 

    public static void main(String[] args) 
        new PassByValueObjectCase2().caller();
    

    public void caller() 
        // student has the actual reference to a Student object created
        // can we change this actual reference outside the local scope? Let's see
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student); // Will it print Nikhil or Anand?
    

    public String method(Student student) 
        student = new Student(20, "Anand");
        return "output";
    


结果

output : output
student : Student [id=10, name=Nikhil]

【讨论】:

【参考方案22】:

在 Java 中你永远不能通过引用传递,其中一种显而易见的方式是当你想从一个方法调用中返回多个值时。考虑以下 C++ 代码:

void getValues(int& arg1, int& arg2) 
    arg1 = 1;
    arg2 = 2;

void caller() 
    int x;
    int y;
    getValues(x, y);
    cout << "Result: " << x << " " << y << endl;

有时你想在 Java 中使用相同的模式,但你不能;至少不是直接的。相反,您可以这样做:

void getValues(int[] arg1, int[] arg2) 
    arg1[0] = 1;
    arg2[0] = 2;

void caller() 
    int[] x = new int[1];
    int[] y = new int[1];
    getValues(x, y);
    System.out.println("Result: " + x[0] + " " + y[0]);

正如之前的答案中所解释的,在 Java 中,您将指向数组的指针作为值传递给 getValues。这已经足够了,因为该方法随后会修改数组元素,并且按照惯例,您希望元素 0 包含返回值。显然,您可以通过其他方式来执行此操作,例如构建您的代码以使这不是必需的,或者构造一个可以包含返回值或允许设置它的类。但是上面 C++ 中提供给您的简单模式在 Java 中不可用。

【讨论】:

【参考方案23】:

我想我会贡献这个答案来添加规范中的更多细节。

首先,What's the difference between passing by reference vs. passing by value?

通过引用传递意味着被调用函数的参数将是 与调用者传递的参数相同(不是值,而是身份

变量本身)。

按值传递意味着被调用函数的参数将是 调用者传递的参数。

或来自***,on the subject of pass-by-reference

在调用引用评估中(也称为 pass-by-reference),函数接收到一个隐式引用 变量用作参数,而不是其值的副本。这 通常意味着该函数可以修改(即分配给) 用作参数的变量——调用者可以看到的东西。

还有on the subject of pass-by-value

在按值调用中,参数表达式被计算,并且 结果值绑定到函数 [...] 中的相应变量。 如果函数或过程能够为其赋值 参数,仅分配其本地副本 [...]。

其次,我们需要知道 Java 在其方法调用中使用什么。 Java Language Specification 状态

当调用方法或构造函数时(第 15.12 节), 实际参数表达式初始化新创建的参数 变量,每个声明的类型,在执行主体之前 方法或构造函数。

因此它将参数的值分配(或绑定)到相应的参数变量。

参数的值是多少?

让我们考虑引用类型,Java Virtual Machine Specification 状态

引用类型分为三种:类类型、数组类型、 和接口类型。 它们的值是动态引用 创建的类实例、数组或类实例或数组 分别实现接口。

Java Language Specification 也表示

引用值(通常只是引用)是指向这些对象的指针,以及一个特殊的空引用,它不引用任何对象。

参数(某些引用类型)的值是指向对象的指针。请注意,变量、具有引用类型返回类型的方法调用以及实例创建表达式 (new ...) 都解析为引用类型值。

所以

public void method (String param) 
...
String variable = new String("ref");
method(variable);
method(variable.toString());
method(new String("ref"));

all 将对String 实例的引用值绑定到方法的新创建参数param。这正是 pass-by-value 的定义所描述的。因此,Java 是按值传递的

你可以按照引用来调用方法或访问被引用对象的字段这一事实与对话完全无关。传递引用的定义是

这通常意味着函数可以修改(即分配给) 用作参数的变量——调用者可以看到的东西。

在 Java 中,修改变量意味着重新分配它。在 Java 中,如果您在方法中重新分配变量,调用者不会注意到它。 修改变量引用的对象是完全不同的概念。


Java 虚拟机规范here 中也定义了原始值。该类型的值是相应的整数或浮点值,经过适当编码(8、16、32、64 等位)。

【讨论】:

【参考方案24】:

区别,或者也许只是我记忆中的方式,因为我曾经与原始海报的印象相同:Java 总是通过价值传递。 Java 中的所有对象(在 Java 中,除了原语之外的任何对象)都是引用。这些引用是按值传递的。

【讨论】:

【参考方案25】:

正如很多人之前提到的,Java is always pass-by-value

这里有另一个例子可以帮助你理解差异 (the classic swap example):

public class Test 
  public static void main(String[] args) 
    Integer a = new Integer(2);
    Integer b = new Integer(3);
    System.out.println("Before: a = " + a + ", b = " + b);
    swap(a,b);
    System.out.println("After: a = " + a + ", b = " + b);
  

  public static swap(Integer iA, Integer iB) 
    Integer tmp = iA;
    iA = iB;
    iB = tmp;
  

打印:

之前:a = 2,b = 3 之后:a = 2,b = 3

发生这种情况是因为 iA 和 iB 是新的局部引用变量,它们具有与传递的引用相同的值(它们分别指向 a 和 b)。因此,尝试更改 iA 或 iB 的引用只会在本地范围内更改,而不是在此方法之外。

【讨论】:

嗨,swap方法的返回类型是什么? @Priyanka Ha!这么多年过去了,你是第一个发现它的人!无效。 语法:“pass-by-value”是句子的宾语吗?【参考方案26】:

我一直认为它是“通过副本”。它是原始值或引用值的副本。如果它是原始数据,则它是作为值的位的副本,如果它是对象,则它是引用的副本。

public class PassByCopy
    public static void changeName(Dog d)
        d.name = "Fido";
    
    public static void main(String[] args)
        Dog d = new Dog("Maxx");
        System.out.println("name= "+ d.name);
        changeName(d);
        System.out.println("name= "+ d.name);
    

class Dog
    public String name;
    public Dog(String s)
        this.name = s;
    

java PassByCopy 的输出:

名称=马克斯 名称=菲多

原始包装类和字符串是不可变的,因此使用这些类型的任何示例都不会与其他类型/对象一样工作。

【讨论】:

【参考方案27】:

与其他一些语言不同,Java 不允许您在按值传递和按引用传递之间进行选择——所有参数都按值传递。方法调用可以将两种类型的值传递给方法——原始值的副本(例如 int 和 double 的值)和对象引用的副本。

当方法修改原始类型参数时,对参数的更改不会影响调用方法中的原始参数值。

当涉及到对象时,对象本身不能传递给方法。所以我们传递对象的引用(地址)。我们可以使用这个引用来操作原始对象。

Java 如何创建和存储对象:当我们创建一个对象时,我们将对象的地址存储在一个引用变量中。下面我们来分析一下。

Account account1 = new Account();

“Account account1”是引用变量的类型和名称,“=”是赋值运算符,“new”要求系统提供所需的空间量。创建对象的关键字 new 右侧的构造函数由关键字 new 隐式调用。使用赋值运算符将创建对象的地址(右值的结果,这是一个称为“类实例创建表达式”的表达式)分配给左值(这是一个指定名称和类型的引用变量)。

虽然对象的引用是按值传递的,但方法仍然可以通过使用对象引用的副本调用其公共方法来与被引用对象交互。由于存储在参数中的引用是作为参数传递的引用的副本,因此被调用方法中的参数和调用方法中的参数引用内存中的同一对象。

出于性能原因,传递对数组的引用而不是数组对象本身是有意义的。因为Java中的一切都是按值传递的,如果传递数组对象, 将传递每个元素的副本。对于大型阵列,这将浪费时间并消耗 用于元素副本的大量存储空间。

在下图中,您可以看到我们在 main 方法中有两个引用变量(这些在 C/C++ 中称为指针,我认为这个术语更容易理解这个特性。)。原始变量和参考变量保存在堆栈内存中(下图中的左侧)。 array1 和 array2 引用变量“point”(如 C/C++ 程序员所称)或分别引用 a 和 b 数组,它们是堆内存中的对象(这些引用变量保存的值是对象的地址)(下图中的右侧) .

如果我们将 array1 引用变量的值作为参数传递给 reverseArray 方法,则在该方法中创建一个引用变量,并且该引用变量开始指向同一个数组 (a)。

public class Test

    public static void reverseArray(int[] array1)
    
        // ...
    

    public static void main(String[] args)
    
        int[] array1 =  1, 10, -7 ;
        int[] array2 =  5, -190, 0 ;

        reverseArray(array1);
    

所以,如果我们说

array1[0] = 5;

在reverseArray方法中,它会改变数组a。

我们在 reverseArray 方法 (array2) 中有另一个引用变量,它指向一个数组 c。如果我们说

array1 = array2;

在reverseArray方法中,reverseArray方法中的引用变量array1将停止指向数组a并开始指向数组c(第二张图中的虚线)。

如果我们将引用变量array2的值作为reverseArray方法的返回值,并将这个值赋给main方法中的引用变量array1,main中的array1将开始指向数组c。

所以让我们现在一次写下我们所做的所有事情。

public class Test

    public static int[] reverseArray(int[] array1)
    
        int[] array2 =  -7, 0, -1 ;

        array1[0] = 5; // array a becomes 5, 10, -7

        array1 = array2; /* array1 of reverseArray starts
          pointing to c instead of a (not shown in image below) */
        return array2;
    

    public static void main(String[] args)
    
        int[] array1 =  1, 10, -7 ;
        int[] array2 =  5, -190, 0 ;

        array1 = reverseArray(array1); /* array1 of 
         main starts pointing to c instead of a */
    

现在 reverseArray 方法结束了,它的引用变量(array1 和 array2)已经消失了。这意味着我们现在在主方法 array1 和 array2 中只有两个引用变量,它们分别指向 c 和 b 数组。没有引用变量指向对象(数组) a。所以它有资格进行垃圾回收。

您还可以将 main 中的 array2 的值​​分配给 array1。 array1 将开始指向 b。

【讨论】:

【参考方案28】:

Java 只能通过值传递。一个非常简单的例子来验证这一点。

public void test() 
    MyClass obj = null;
    init(obj);
    //After calling init method, obj still points to null
    //this is because obj is passed as value and not as reference.

private void init(MyClass objVar) 
    objVar = new MyClass();

【讨论】:

【参考方案29】:

长话短说,Java 对象有一些非常奇特的属性。

通常,Java 具有直接按值传递的原始类型(intboolchardouble 等)。然后Java 有对象(从java.lang.Object 派生的所有东西)。对象实际上总是通过引用处理(引用是您无法触摸的指针)。这意味着实际上,对象是通过引用传递的,因为引用通常是不感兴趣的。但是,这确实意味着您无法更改指向哪个对象,因为引用本身是按值传递的。

这听起来很奇怪和令人困惑吗?让我们考虑一下 C 是如何实现按引用传递和按值传递的。在 C 中,默认约定是按值传递。 void foo(int x) 按值传递一个 int。 void foo(int *x) 是一个不需要 int a 的函数,而是一个指向 int 的指针:foo(&amp;a)。可以将它与&amp; 运算符一起使用来传递变量地址。

把它带到 C++ 中,我们有参考。引用基本上(在这种情况下)是隐藏等式指针部分的语法糖:void foo(int &amp;x)foo(a) 调用,其中编译器本身知道它是一个引用和非引用a 的地址应该通过。在 Java 中,所有引用对象的变量实际上都是引用类型,实际上是在没有 C++ 等提供的细粒度控制(和复杂性)的情况下强制按引用调用大多数意图和目的。

【讨论】:

这是错误的。 Java 称之为“引用”的东西 C++ 称之为“指针”。 Java 中不存在 C++ 所称的“引用”。 C++ 引用是类似类型的指针,但具有全局范围。当您更改 C++ 引用时,所有出现的引用都会更改,无论是在被调用函数中还是在调用函数中。 Java 无法做到这一点。 Java 是严格按值传递的,对 java 引用的更改是严格本地的。 Java被调用函数不能改变调用函数的引用值。您可以使用 AtomicReference 等包装对象来模拟 C++ 引用。 C++ 引用与作用域无关。在实现中,它们就像不允许具有空值的指针。除此之外的主要区别在于,它们在语法上表现为引用数据的别名。在 Java 中引用的工作方式几乎相同,但有一些特殊规则允许:与 null 和其他引用值进行比较(使用 == 运算符)。 C++ 也是按值传递的,尽管该值可以是引用的指针/引用。 调用方法对 C++ 引用所做的更改也可以通过调用方法看到。这在 Java 中不存在,也不是类似行为的指针。在 Java 和 C 中,对指针值的更改仅是本地的。我不知道如何正确调用这种行为,但它有点类似于某些脚本语言的“外部范围”。 正确传递引用的示例请参见此处的交换程序:geeksforgeeks.org/references-in-c 在 Java 中编写具有相同副作用的交换方法是不可能的。 Java 引用或 C 指针中不存在 C++ 引用的“质量”(语言运算符的行为)。 @Paul de Vrieze“不允许有空值”-想想,在 C 方言中,当 p 是指针时,*p 是引用;这是有效的,即使 p 为空。关于赋值,Java 中的引用表现得像指针,并且符合 C 的“按引用调用”语义。【参考方案30】:

我创建了一个专门针对任何编程语言here的此类问题的线程。

Java is also mentioned。这是简短的摘要:

Java 按值传递参数 “按值”是java中向方法传递参数的唯一方式 使用作为参数给出的对象中的方法将改变 对象作为引用指向 原始对象。 (如果说 方法本身会改变一些值)

【讨论】:

以上是关于Java 是“按引用传递”还是“按值传递”?的主要内容,如果未能解决你的问题,请参考以下文章

Java的参数传递是「按值传递」还是「按引用传递」?

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

.bind(this) 是按引用传递还是按值传递?

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

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

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