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

Posted

技术标签:

【中文标题】Java 是“按引用传递”还是“按值传递”?【英文标题】:Is Java "pass-by-reference" or "pass-by-value"? 【发布时间】:2015-04-07 16:35:37 【问题描述】:

我一直认为 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 开发人员,我想知道 Java 是使用 Call-by-Value 还是 Call-by-Reference?”

对此没有普遍的答案,也不可能因为这是一个错误的二分法。我们有 2 个不同的术语(按值调用/按引用调用),但在将数据传递给方法时至少有 3(!)种不同的方式来处理所述数据:

    我们的数据被复制,并将副本提供给方法。对副本的更改不会传播到外部。(想想 Java 或 C++ 或 C# 中的 int。) 我们数据的指针(内存地址)被提供给方法。对我们数据的更改确实会传播到外部。我们也可以指向一些新的实例,让我们的原始数据悬空。 (想想 C++ 中的指针。) 和 #2 一样,除了我们只能更改原始数据,而不能更改指针指向的内容。 (想想在 Java 中作为参数传递的实例。)

在 OO 世界中,#1 是 Call-by-Value 而#2 是 Call-by-Reference 是没有争议的。 但是,由于我们对三个选项只有两个术语,因此由于选项 #3,这两个术语之间没有明确的界限。

“这对我意味着什么?”

这意味着在 Java 领域内,您可以合理地假设 #3 被假设为 Call-by-Value(或更口头的体操术语,Call-References-by-Value)。

但是,在更广泛的 OO 世界中,这意味着您必须要求在 Call-by-Value 和 Call-by-Reference 之间进行显式描述,特别是对方如何分类 #3。

“但我读到 JLS 将其定义为 Call-by-Value!”

这就是为什么您在与 Java 开发人员打交道时最初的假设应该是上述的原因。然而,JLS 在 Java 之外的权限要小得多。

“为什么 Java 开发人员要坚持使用自己的术语?”

我不能推测,但我认为指出 #2 存在一些潜在问题是公平的(显然是 Call-by-Reference),正如所暗示的那样,导致 Call-by-Reference 没有到处都是最好的声誉。

“有办法解决这个烂摊子吗?”

3 个选项,只有 2 个普遍存在的名称。明显的退出是第三个术语,它获得了与 Call-by-Value 和 Call-by-Reference 相同的广泛使用(并且它比 Call-References-by-Value 更容易混淆,也许Call-by-Sharing)。在此之前,您需要假设或询问,如上所述。

归根结底,叫什么都无所谓,只要我们彼此了解,不混淆就行。

【讨论】:

正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center。 选项 3:您认为对复制对象的引用?认为混乱在于,许多响应根本拒绝考虑对象,并且在 Java 之前很久就使用了传递实体的模式。 有了#3,我在想Java在做什么。在 Java 中,当您的方法通过实例 Foo foo 并分配 foo = new FancyFooSubclass(); 时,这些更改不会传播到外部。在您的方法返回后,foo 仍然指向原始的 Foo 对象。例如,传递给方法的 C++ 指针不一定是这种情况。这就是为什么原始问题提出了错误的二分法。并且有数百个赞成票 (!) 的答案并没有消除这种误解,这加剧了初级开发人员的混乱状态。 在 Java 中分配参数引用与在 C 中分配参数指针的语义相同(不包括 C++ 指针引用,对于某些类型 T,“T*&”)。当然同意你,多数不一定符合道理。二分法存在于语法之后。问题下方是带有合理链接的评论,让事情变得直截了当-> baeldung.com/java-pass-by-value-or-pass-by-reference 为了进一步娱乐,您可以关注我对该主题的最后回复:) 就个人而言,我在游戏中没有皮肤。我不太喜欢将传递值类型称为 CbV,将引用类型称为 CbR。 Java开发者的解释。 otoh,与 CbV 和 CbR 中使用的实际单词分离。但归根结底,我们使用哪些表达方式并不像拥有足够的表达方式来表达自己那么重要。传递参数当然具有比两个术语表达的更多不同的属性:docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/…【参考方案2】:

经过详尽的讨论后,我认为是时候将所有严肃的结果放在一个 sn-p 中了。

/**
 * 
 * @author Sam Ginrich
 * 
 * All Rights Reserved!
 * 
 */
public class JavaIsPassByValue


    static class SomeClass
    
        int someValue;

        public SomeClass(int someValue)
        
            this.someValue = someValue;
        
    

    static void passReferenceByValue(SomeClass someObject)
    
        if (someObject == null)
        
            throw new NullPointerException(
                    "This Object Reference was passed by Value,\r\n   that's why you don't get a value from it.");
        
        someObject.someValue = 49;
    

    public static void main(String[] args)
    
        SomeClass someObject = new SomeClass(27);
        System.out.println("Here is the original value: " + someObject.someValue);

        passReferenceByValue(someObject);
        System.out.println(
                "\nAs ´Java is pass by value´,\r\n   everything without exception is passed by value\r\n   and so an object's attribute cannot change: "
                    + someObject.someValue);

        System.out.println();
        passReferenceByValue(null);
    

从输出中可以很容易看出,在Java中一切都是按值传递的,太简单了!

Here is the original value: 27

As ´Java is pass by value´,
   everything without exception is passed by value
   and so an object´s attribute cannot change: 49

'Exception in thread "main" java.lang.NullPointerException: This Object Reference was passed by value,
   that´s why you don´t get a value from it. 
    at JavaIsPassByValue.passReferenceByValue(JavaIsPassByValue.java:26)
    at JavaIsPassByValue.main(JavaIsPassByValue.java:43)

【讨论】:

【参考方案3】:

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

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

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

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

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

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

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

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

【讨论】:

【参考方案4】:

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

首先,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 等位)。

【讨论】:

【参考方案5】:

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”已经不在我们中间了【参考方案6】:

猜一猜,由于语言不准确,通用正典是错误的

编程语言的作者无权重命名已建立的编程概念。

原始 Java 类型 byte, char, short, int, long float, double 肯定是按值传递的。

所有其他类型都是Objects:对象成员和参数在技术上是引用

所以这些“引用”是“按值”传递的,但堆栈上不会发生对象构造。对象成员(或数组中的元素)的任何更改都适用于相同的原始对象;这样的引用恰好符合在任何 C 方言中传递给某个函数的实例指针的逻辑,我们以前称之为 通过引用传递对象

特别是我们确实有这个东西 java.lang.NullPointerException,这在纯粹的按值概念中没有意义

【讨论】:

即使值本身是引用,它仍然是按值传递。参数被复制并重新分配它不会影响被调用函数之外的任何内容 - 这是按值传递/引用之间的关键区别。 我们同意,引用和对象是两双鞋 这对我来说似乎是正确的答案。为什么人们如此关注传递对象引用与传递引用的技术细节?如果将非原始变量传递给函数,则函数内对该变量的修改会影响函数外的变量。这与传递引用的行为几乎相同。 “即使值本身是引用,它仍然是按值传递的” - 嗯,好的,但是如果值是引用,那么您正在传递引用。抛开术语不谈,将对象作为参数传递会影响函数外部的对象。【参考方案7】:

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
Let's understand step by step:

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

第一个插图

new PassByValue().changeValue(t);

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

第二幅插图

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

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

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,如下所示:

第三幅插图

希望这会有所帮助。

【讨论】:

“既然我们说 t 的值为 0x100234,那么 t 和 f 将具有相同的值,因此它们将指向同一个对象。” - 这听起来不对。创建对象的副本并不意味着副本和原始对象都指向同一个对象。这闻起来像按引用传递,或者至少是按对象引用传递【参考方案8】:

Java 使用按值传递,但无论您使用原始类型还是引用类型,效果都会有所不同。

当您将原始类型作为参数传递给方法时,它会获取原始类型的副本,并且方法块内的任何更改都不会更改原始变量。

当您将引用类型作为参数传递给方法时,它仍然会得到一个副本,但它是对对象的引用的副本(换句话说,您将获得内存地址的副本对象所在的堆),所以方法的块内对象的任何变化都会影响块外的原始对象。

【讨论】:

【参考方案9】:

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 实际上包含里面文件的原始地址).. 所以为了更好地理解制作快捷方式文件和感觉。

【讨论】:

【参考方案10】:

我刚刚注意到你引用了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 好点 - 我已经澄清了(如果仍然不清楚,请再次评论)【参考方案11】:

为了简单和冗长 它的pass reference by value

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

【讨论】:

当然,混淆来自于这样一个事实,即在 Java 社区之外还有其他语义和冗长的“Java 不是上帝”【参考方案12】:

这里的每一个答案都是通过引用其他语言来获取传递指针,并说明在 Java 中这是不可能做到的。无论出于何种原因,没有人试图展示如何从其他语言实现按值传递对象。

这段代码展示了如何做到这一点:

public class Test

    private static void needValue(SomeObject so) throws CloneNotSupportedException
    
        SomeObject internalObject = so.clone();
        so=null;
        
        // now we can edit internalObject safely.
        internalObject.set(999);
    
    public static void main(String[] args)
    
        SomeObject o = new SomeObject(5);
        System.out.println(o);
        try
        
            needValue(o);
        
        catch(CloneNotSupportedException e)
        
            System.out.println("Apparently we cannot clone this");
        
        System.out.println(o);
    


public class SomeObject implements Cloneable

    private int val;
    public SomeObject(int val)
    
        this.val = val;
    
    public void set(int val)
    
        this.val = val;
    
    public SomeObject clone()
    
        return new SomeObject(val);
    
    public String toString()
    
        return Integer.toString(val);
    

这里我们有一个函数needValue,它的作用是立即创建一个对象的克隆,这需要在对象本身的类中实现,并且需要将类标记为Cloneable。在此之后将so 设置为null 不是必需的,但我在这里这样做是为了表明我们不会在那之后使用该引用。

Java 很可能没有按引用传递的语义,但称该语言为“按值传递”是一厢情愿的想法。

【讨论】:

注意:“现在我们可以安全地编辑 internalObject”中的 safely 取决于 clone() 方法的实现(即“深”或“浅”)以及如何在对象树的最下方,您的编辑会产生影响。对于这个简单的例子,它是正确的。 猜猜,你是第一个在 Java 中成功地“通过值传递对象”的人,以后会称你为“Java 的尼尔·阿姆斯特朗”;)【参考方案13】:

这里有一个更精确的定义:

按值传递/调用: 形参就像一个局部变量 功能范围,它在 函数调用。 通过引用传递/调用: 形参只是实参的别名 值,它在功能范围内的任何变化都可以有边 在代码的任何其他部分产生外部影响。

因此,在 C/C++ 中,您可以创建一个函数来交换使用引用传递的两个值:

void swap(int& a, int& b) 

    int tmp = a; 
    a = b; 
    b = tmp; 

你可以看到它对 a 和 b 有一个唯一的引用,所以我们没有副本,tmp 只是保存唯一的引用。

java中同样的函数没有副作用,参数传递就像上面的代码一样没有引用。

虽然 java 使用指针/引用,但参数不是唯一的指针,在每个属性中,它们被复制而不是像 C/C++ 一样分配

【讨论】:

【参考方案14】:

只有两个版本:

您可以传递值,即 (4,5) 您可以传递一个地址,即 0xF43A

Java 将原始值作为值传递,将对象作为地址传递。那些说“地址也是价值”的人并没有区分两者。那些关注交换函数效果的人关注的是传递完成后会发生什么。

在 C++ 中,您可以执行以下操作:

Point p = Point(4,5);

这会在堆栈上保留 8 个字节并在其中存储 (4,5)。

Point *x = &p;

这会在堆栈上保留 4 个字节,并在其中存储 0xF43A。

Point &y = p;

这会在堆栈上保留 4 个字节,并在其中存储 0xF43A。

    我想每个人都会同意,如果 f 的定义是 f(Point p),那么对 f(p) 的调用就是按值传递。在这种情况下,额外的 8 个字节被保留,并且 (4,5) 被复制到其中。当f改变p时,保证f返回时原件不变。

    如果 f 的定义是 f(Point &p),我想每个人都会同意对 f(p) 的调用是传递引用。在这种情况下,额外的 4 个字节被保留,0xF43A 被复制到其中。当 f 改变 p 时,保证在 f 返回时改变原来的。

    如果 f 的定义是 f(Point *p),则对 f(&p) 的调用也是传递引用。在这种情况下,额外的 4 个字节被保留,0xF43A 被复制到其中。当 f 改变 *p 时,保证在 f 返回时改变原来的。

    如果 f 的定义是 f(Point *p),则对 f(x) 的调用也是传递引用。在这种情况下,额外的 4 个字节被保留,0xF43A 被复制到其中。当 f 改变 *p 时,保证在 f 返回时改变原来的。

    如果 f 的定义是 f(Point &p),则对 f(y) 的调用也是传递引用。在这种情况下,额外的 4 个字节被保留,0xF43A 被复制到其中。当 f 改变 p 时,保证在 f 返回时改变原来的。

当然,传递完成后会发生什么不同,但这只是一种语言结构。在指针的情况下,您必须使用 -> 来访问成员,而在引用的情况下,您必须使用 .. 如果要交换原始值,则可以执行 tmp=a; a=b; b=tmp;在引用和 tmp=*a 的情况下; *b=tmp; *a=tmp 用于指针。而在 Java 中你会这样做: tmp.set(a); a.set(b); b. 设置(tmp)。专注于赋值语句是愚蠢的。如果你写一点代码,你可以在 Java 中做同样的事情。

Java 通过值传递原始值,通过引用传递对象。 Java 复制值来实现这一点,但 C++ 也是如此。

为了完整性:

Point p = new Point(4,5);

这会在堆栈上保留 4 个字节并在其中存储 0xF43A 并在堆上保留 8 个字节并在其中存储 (4,5)。

如果你想像这样交换内存位置

void swap(int& a, int& b) 
    int *tmp = &a;
    &a = &b;
    &b = tmp;

然后你会发现你遇到了硬件的限制。

【讨论】:

最完整的答案之一被撞到了底部。您的#3 最好地描述了 Java 中的情况。 与语义无关:猜猜,所有Java实现都使用两级间接,即引用是一种句柄,还不是对象地址,可能会因垃圾回收而改变。【参考方案15】:

如果你想让它放在一个句子中以便于理解和记住,最简单的答案:

Java 总是用新的引用传递值

(所以你可以修改原始对象但不能访问原始引用)

【讨论】:

对象可以被修改,因为它是通过它的引用传递的;)【参考方案16】:

不再赘述,但对那些看了很多答案后可能仍然感到困惑的人来说一点:

Java 中的 pass by value 不等于 与 C++ 中的 pass by value 相同,虽然听起来是这样,这可能就是造成混淆的原因

分解:

pass by value 在 C++ 中表示传递对象的值(如果是对象),字面意思是对象的副本 Java 中的pass by value 表示传递对象的地址值(如果是对象),而不是像 C++ 那样真正传递对象的“值”(副本) Java 中的pass by value 对函数内部的对象(例如myObj.setName("new"))进行操作对函数外部的对象产生影响pass by value 在 C++ 中,它对外部有 NO 影响。 然而,pass by reference 在 C++ 中,在函数 DOES 中对对象进行操作会对外部对象产生影响!与 Java 中的 pass by value 相似(只是相似,不一样),不是吗?.. 人们总是重复“Java 中没有引用传递”,=> 轰隆隆,混乱开始了……

所以,朋友们,一切都只是术语定义的差异(跨语言),您只需要知道它是如何工作的,就是这样(虽然有时有点令人困惑叫我承认)!

【讨论】:

在两种语言中,“按值传递”意味着传递对象的值(如果它是一个对象)。在任何情况下,这都不意味着传递值的地址。术语没有区别。没有任何情况下我们通过值调用某些东西并更改被调用函数中的值会更改调用者中的值。如果你在java中调用像foo(bar)这样的函数,bar的值永远不会改变——如果bar是一个对象的引用,它的值就是它引用的对象的标识,不管函数是什么确实,它仍然在调用者中引用同一个对象。 我同意 David Schwartz 的观点:术语实际上没有区别。 Java 和 C++ 之间的最大区别在于,在 Java 中,“值”可以永远是一个完整的对象。它始终是引用或原始值。您根本无法在 Java 中传递整个对象。【参考方案17】:

我换一种说法:

在 java 中,引用被传递(但不是对象)并且这些引用是按值传递的(引用本身被复制,因此您有 2 个引用,并且您无法控制方法中的第一个引用)。

只是说:按值传递对于初学者来说可能不够清楚。例如在 Python 中同样的情况,但是有文章描述他们称之为pass-by-reference,只是因为使用了引用。

【讨论】:

【参考方案18】:

我看到所有答案都包含相同的内容:按值传递。然而,最近关于 Valhalla 项目的 Brian Goetz 更新实际上给出了不同的答案:

确实,关于 Java 对象是按值传递还是按引用传递,这是一个常见的“陷阱”问题,答案是“都不是”:对象引用是按值传递的。

您可以在这里阅读更多内容:State of Valhalla. Section 2: Language Model

编辑:Brian Goetz 是 Java 语言架构师,领导着 Project Valhalla 和 Project Amber 等项目。

Edit-2020-12-08:更新State of Valhalla

【讨论】:

对象引用(即指针)按值传递,原语也按值传递。这意味着一切总是按值传递。我认为这里的操作术语是按值传递。 我认为领导 Amber 项目和 Valhalla 项目的 Java 语言架构师是一个可靠的消息来源,声称它不是价值传递。 首先,我不认为他比 Java 的创造者 James Gosling 更可信,他在《Java 编程语言》一书中明确指出,Java 确实是按值传递 (第 2 章,第 2.6.5 节)。其次,虽然 Goetz 说它既不是 PBV 也不是 PBR,但他接着说引用是 PASSED BY VALUE,从而自相矛盾。如果您了解 Java,那么您也知道原语也是 PASSED BY VALUE。由于 Java 中的所有内容都是按值传递的,因此 Java 是一种按值传递的语言。 其他比 Goetz 更可信的消息来源是 Aho、Lam、Sethi 和 Ullman,他们以“编译器:原理、技术和工具”一书而闻名,这是标准的大学教科书编译器构造。本书第 2 版第 1.6.6 节也指出 Java 是按值传递的。 最相关的参考是 Java 语言规范,它声明 “这样做的效果是将参数值分配给方法的相应新创建的参数变量” . (15.12.4.5)。请注意,完全不说“经过……”是为了避免术语混淆。 (FWIW,我不同意 Goetz 对“按值传递”和“按值传递引用”的语义不同的描述。我同意他自相矛盾。)【参考方案19】:

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 的书教过这个 “对象不是引用”,真的吗?【参考方案20】:

Java 总是按值传递参数。 Java 中的所有对象引用都是按值传递的。这意味着将值的副本传递给方法。但诀窍在于,传递值的副本也会改变对象的真实值。

请参考下面的例子,

public class ObjectReferenceExample 

    public static void main(String... doYourBest) 
            Student student = new Student();
            transformIntoHomer(student);
            System.out.println(student.name);
    

    static void transformIntoDuleepa(Student student) 
            student.name = "Duleepa";
    

class Student 
    String name;

在这种情况下,它将是 Duleepa! 原因是Java对象变量只是指向内存堆中真实对象的简单引用。 因此,即使Java通过值向方法传递参数,如果变量指向对象引用,真实的对象也会发生变化。

【讨论】:

运行这个程序应该可以帮助任何人理解这个概念。 pastebin.com/VEc8NQcX 在这个例子中,你没有复制 Student 对象,所以我认为说“传递值的副本也会改变对象的真实值”是一种误导。您正在复制和按值传递的东西是对象的 reference。该对象仍然存在于堆中,并且只有其中一个。这就是为什么当你使用新的引用来改变对象时,你是在为所有其他拥有该对象引用的人改变它。只有一个对象。当函数的参数是原始类型而不是对对象的引用时,它也会被复制。【参考方案21】:

与其他一些语言不同,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。

【讨论】:

【参考方案22】:

已经有很好的答案涵盖了这一点。我想通过分享一个非常简单的示例(将编译)来对比 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++ 以及大多数其他语言中的选项。

【讨论】:

【参考方案23】:

A:Java 确实通过引用来操作对象,并且所有对象变量都是引用。但是,Java 不会通过引用传递方法参数。它按值传递它们。

以 badSwap() 方法为例:

public void badSwap(int var1, int var2)

  int temp = var1;
  var1 = var2;
  var2 = temp;

当 badSwap() 返回时,作为参数传递的变量仍将保持其原始值。如果我们将参数类型从 int 更改为 Object,该方法也会失败,因为 Java 也通过值传递对象引用。现在,这里是棘手的地方:

public 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("X: " + pnt1.x + " Y: " +pnt1.y); 
  System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);
  System.out.println(" ");
  tricky(pnt1,pnt2);
  System.out.println("X: " + pnt1.x + " Y:" + pnt1.y); 
  System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);  

如果我们执行这个 main() 方法,我们会看到以下输出:

X: 0 Y: 0
X: 0 Y: 0
X: 100 Y: 100
X: 0 Y: 0

该方法成功地改变了 pnt1 的值,即使它是按值传递的;但是,pnt1 和 pnt2 的交换失败了!这是混乱的主要来源。在 main() 方法中,pnt1 和 pnt2 只不过是对象引用。当您将 pnt1 和 pnt2 传递给 tricky() 方法时,Java 按值传递引用,就像任何其他参数一样。这意味着传递给该方法的引用实际上是原始引用的副本。下面的图 1 显示了在 Java 将对象传递给方法后指向同一个对象的两个引用。

图 1. 传递给方法后,一个对象将至少有两个引用

Java 按值复制和传递引用,而不是对象。因此,方法操作将改变对象,因为引用指向原始对象。但是由于引用是副本,交换会失败。方法引用交换,而不是原始引用。不幸的是,在方法调用之后,您只剩下未交换的原始引用。为了在方法调用之外成功进行交换,我们需要交换原始引用,而不是副本。

【讨论】:

【参考方案24】:

我认为这个简单的解释可能会帮助您理解,因为当我在努力解决这个问题时,我想理解同样的事情。

当你将原始数据传递给函数调用时,它的内容被复制到函数的参数中,当你传递一个对象时,它的引用被复制到函数的参数中。说到对象,您无法更改复制的引用-参数变量在调用函数中引用

考虑这个简单的例子,String 是 java 中的一个对象,当您更改字符串的内容时,引用变量现在将指向一些新的引用,因为 String 对象在 java 中是不可变的。

String name="Mehrose";  // name referencing to 100

ChangeContenet(String name)
 name="Michael"; // refernce has changed to 1001

 
System.out.print(name);  //displays Mehrose

相当简单,因为正如我所提到的,您不能在调用函数中更改复制的引用。但是当你传递一个字符串/对象数组时,问题出在数组上。让我们看看。

String names[]="Mehrose","Michael";

changeContent(String[] names)
  names[0]="Rose";
  names[1]="Janet"



System.out.println(Arrays.toString(names)); //displays [Rose,Janet]

正如我们所说,我们不能在函数调用中更改复制的引用,我们也看到了单个 String 对象的情况。原因是 names[] 变量引用了 200,而 names[0] 引用了 205,依此类推。您会看到我们没有更改 names[] 引用,它在函数调用之后仍然指向旧的相同引用,但现在 names[0] 和 names[1] 引用已更改。我们仍然坚持我们的定义,即我们不能更改引用变量的引用,所以我们没有。

当您将 Student 对象传递给方法并且您仍然可以更改 Student 名称或其他属性时,也会发生同样的事情,关键是我们没有更改实际的 Student 对象,而是更改了它的内容

你不能这样做

Student student1= new Student("Mehrose");

changeContent(Student Obj)
 obj= new Student("Michael") //invalid
 obj.setName("Michael")  //valid


【讨论】:

【参考方案25】:
public class Test 

    static class Dog 
        String name;

        @Override
        public int hashCode() 
            final int prime = 31;
            int result = 1;
            result = prime * result + ((name == null) ? 0 : name.hashCode());
            return result;
        

        @Override
        public boolean equals(Object obj) 
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Dog other = (Dog) obj;
            if (name == null) 
                if (other.name != null)
                    return false;
             else if (!name.equals(other.name))
                return false;
            return true;
        

        public String getName() 
            return name;
        

        public void setName(String nb) 
            this.name = nb;
        

        Dog(String sd) 
            this.name = sd;
        
    
    /**
     * 
     * @param args
     */
    public static void main(String[] args) 
        Dog aDog = new Dog("Max");

        // we pass the object to foo
        foo(aDog);
        Dog oldDog = aDog;

        System.out.println(" 1: " + aDog.getName().equals("Max")); // false
        System.out.println(" 2 " + aDog.getName().equals("huahua")); // false
        System.out.println(" 3 " + aDog.getName().equals("moron")); // true
        System.out.println(" 4 " + " " + (aDog == oldDog)); // true

        // part2
        Dog aDog1 = new Dog("Max");

        foo(aDog1, 5);
        Dog oldDog1 = aDog;

        System.out.println(" 5 : " + aDog1.getName().equals("huahua")); // true
        System.out.println(" part2 : " + (aDog1 == oldDog1)); // false

        Dog oldDog2 = foo(aDog1, 5, 6);
        System.out.println(" 6 " + (aDog1 == oldDog2)); // true
        System.out.println(" 7 " + (aDog1 == oldDog)); // false
        System.out.println(" 8 " + (aDog == oldDog2)); // false
    

    /**
     * 
     * @param d
     */
    public static void foo(Dog d) 
        System.out.println(d.getName().equals("Max")); // true

        d.setName("moron");

        d = new Dog("huahua");
        System.out.println(" -:-  " + d.getName().equals("huahua")); // true
    

    /**
     * 
     * @param d
     * @param a
     */
    public static void foo(Dog d, int a) 
        d.getName().equals("Max"); // true

        d.setName("huahua");
    

    /**
     * 
     * @param d
     * @param a
     * @param b
     * @return
     */
    public static Dog foo(Dog d, int a, int b) 
        d.getName().equals("Max"); // true
        d.setName("huahua");
        return d;
    

示例代码演示了对不同功能的对象的影响。

【讨论】:

【参考方案26】:

首先让我们了解 Java 中的内存分配: 堆栈和堆是 JVM 为不同目的分配的内存的一部分。堆栈内存是预先分配给线程的,在创建时,一个线程不能访问其他线程的堆栈。但是堆对程序中的所有线程都可用。

对于一个线程,Stack 存储所有本地数据、程序元数据、原始类型数据和对象引用。并且,堆负责实际对象的存储。

Book book = new Book("Effective Java");

在上面的例子中,引用变量是存储在堆栈中的“book”。 new operator -> new Book("Effective Java") 创建的实例存储在 Heap 中。 ref 变量“book”具有在堆中分配的对象的地址。假设地址是 1001。

考虑传递原始数据类型,即 int、float、double 等。

public class PrimitiveTypeExample  
    public static void main(string[] args) 
       int num = 10;
       System.out.println("Value before calling method: " + num);
       printNum(num);
       System.out.println("Value after calling method: " + num);
    
    public static void printNum(int num)
       num = num + 10;
       System.out.println("Value inside printNum method: " + num);
    

输出是: 调用方法前的值:10 printNum 方法中的值:20 调用方法后的值:10

int 数 =10; -> 这为正在运行的线程的堆栈中的“int”分配内存,因为它是原始类型。现在,当调用 printNum(..) 时,会在同一个线程中创建一个私有堆栈。当“num”被传递给该方法时,“num”的副本会在方法堆栈帧中创建。 数=数+10; -> 这增加了 10 并修改了方法堆栈帧中的 int 变量。 因此,方法堆栈框架外的原始 num 保持不变。

考虑将自定义类的对象作为参数传递的示例。

在上面的例子中,引用变量“book”驻留在执行程序的线程堆栈中,当程序执行new Book()时,类Book的对象是在堆空间中创建的。堆中的这个内存位置由“书”引用。当“book”作为方法参数传递时,“book”的副本会在同一线程堆栈内的方法的私有堆栈框架中创建。因此,复制的引用变量指向堆中“Book”类的同一个对象。

方法栈帧中的引用变量为同一个对象设置了一个新值。因此,它会在原始 ref 变量“book”获取其值时反映出来。 注意,在传递引用变量的情况下,如果在被调用的方法中再次初始化,则指向新的内存位置,任何操作都不会影响堆中的前一个对象。

因此,当任何东西作为方法参数传递时,它始终是 Stack 实体——原始变量或引用变量。我们从不传递存储在堆中的东西。因此,在Java中,我们总是在栈中传递值,而且是按值传递。

【讨论】:

【参考方案27】:

Java按值传递参数,Java中没有传递引用的选项。

但在编译器绑定层,它使用内部引用,不暴露给用户。

这是必不可少的,因为它可以节省大量内存并提高速度。

【讨论】:

每次你传递一个对象时,它都是通过引用传递的,因为这就是你在堆栈上找到的:一个引用【参考方案28】:

与其他一些语言不同,Java 不允许您在按值传递和按引用传递之间进行选择。

所有参数都按值传递。

一个方法调用可以将两个types of values传递给一个方法

原始值的副本(例如,int 和 double 类型的值) 对象引用的副本。

Objects themselves cannot be passed to methods。当方法修改原始类型参数时,对参数的更改不会影响调用方法中的原始参数值。

对于引用类型参数也是如此。如果修改引用类型参数使其引用另一个对象,则只有参数引用新对象——存储在调用者变量中的引用仍然引用原始对象。

参考:Java™ How To Program (Early Objects), Tenth Edition

【讨论】:

对象通过引用传递。 :)【参考方案29】:

长话短说:

    非原始:Java 传递引用的值。 原语:只是价值。

结局。

(2) 太容易了。现在,如果您想考虑 (1) 的含义,假设您有一个 Apple 类:

class Apple 
    private double weight;
    public Apple(double weight) 
        this.weight = weight;
    
    // getters and setters ...


那么当你将这个类的一个实例传递给main方法时:

class Main 
    public static void main(String[] args) 
        Apple apple = new Apple(3.14);
        transmogrify(apple);
        System.out.println(apple.getWeight()+ " the goose drank wine...";

    

    private static void transmogrify(Apple apple) 
        // does something with apple ...
        apple.setWeight(apple.getWeight()+0.55);
    

哦..但您可能知道,您对执行以下操作时会发生什么感兴趣:

class Main 
    public static void main(String[] args) 
        Apple apple = new Apple(3.14);
        transmogrify(apple);
        System.out.println("Who ate my: "+apple.getWeight()); // will it still be 3.14? 

    

    private static void transmogrify(Apple apple) 
        // assign a new apple to the reference passed...
        apple = new Apple(2.71);
    



【讨论】:

【参考方案30】:

Java 仅按值传递。没有通过引用,例如,可以看下面的例子。

package com.asok.cop.example.task;
public class Example 
    int data = 50;

    void change(int data) 
        data = data + 100;// changes will be in the local variable 
        System.out.println("after add " + data);
        

    public static void main(String args[]) 
        Example op = new Example();
        System.out.println("before change " + op.data);
        op.change(500);
        System.out.println("after change " + op.data);
    

输出:

before change 50
after add 600
after change 50

正如迈克尔在 cmets 中所说:

对象仍然是按值传递的,即使对它们的操作类似于按引用传递。考虑void changePerson(Person person) person = new Person(); 调用者对 person 对象的引用将保持不变。对象本身是按值传递的,但它们的成员可能会受到更改的影响。要实现真正的按引用传递,我们必须能够将参数重新分配给新对象,并将更改反映在调用者中。

【讨论】:

将 java 描述为“按值传递”是高度误导的。对于非原始类型,Java 使用“通过引用的值传递”。 “按值传递”意味着在传递给方法时会复制该值。不是,引用被复制了。 不是答案的一半。

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

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

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

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

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

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

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