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


6576

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

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

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

有什么解释?


705
我认为,在这个问题上的许多困惑与以下事实有关:不同的人对“参考”一词有不同的定义。来自C ++背景的人们认为“引用”必须表示其在C ++中的含义,来自C背景的人们认为“引用”必须与他们的语言中的“指针”相同,依此类推。说Java通过引用传递是否正确,实际上取决于“引用”的含义。
重力

119
我尝试一致地使用“ 评估策略”文章中的术语。应当指出的是,尽管本文指出了术语因社区而有很大的不同,但它强调,语义的区别call-by-valuecall-by-reference区别是非常关键的。(我个人比较喜欢用call-by-object-sharing这些天的时间call-by-value[-of-the-reference],因为这是从高层次上描述语义的,并且不会与call-by-value,这基础实现相冲突。)

59
@Gravity:您可以在巨大的广告牌上发表您的评论吗?简而言之,这就是整个问题。它表明整个事情都是语义。如果我们不同意参考的基本定义,那么我们就不会同意这个问题的答案:)
MadConan 2013年

29
我认为混淆是“通过引用传递”与“引用语义”。Java是具有参考语义的按值传递。
spraff 2014年

11
@Gravity,虽然您绝对正确,但来自C ++的人们会本能地对“引用”一词有不同的直觉,但我个人认为,身体更多地隐藏在“ by”中。“通过”令人困惑,因为它与Java中的“通过”绝对不同。在C ++中,通俗地说不是。在C ++中,您可以说“通过参考”,并且可以通过swap(x,y)测试

Answers:


5838

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"。值aDogmain未在功能改变fooDog "Fifi"作为对象基准由值来传递。如果是通过引用传递的,则aDog.getName()in main"Fifi"在调用之后返回foo

同样地:

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

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

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

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


263
这是否会使问题与内部细节混淆?假设您的意思是“指向对象的内部指针的值”,则“传递引用”和“传递引用的值”在概念上没有区别。
izb

383
但是有细微的差别。看第一个例子。如果纯粹通过引用传递,则aDog.name将为“ Fifi”。不是-您获得的引用是一个值引用,如果覆盖该值,则在退出函数时将还原该引用。
erlando's

300
@Lorenzo:不,在Java中,所有内容都是按值传递的。基元按值传递,对象引用按值传递。对象本身永远不会传递给方法,但是对象始终位于堆中,并且只有对该对象的引用才传递给方法。
Esko Luontola,2009年

294
我尝试了一种可视化对象传递的好方法:想象一个气球。调用fxn就像将第二个字符串绑在气球上并将线移到fxn一样。parameter = new Balloon()将剪切该字符串并创建一个新的气球(但这对原始气球没有影响)。parameter.pop()仍会弹出它,因为它跟随字符串到相同的原始气球。Java是按值传递的,但是传递的值不是很深,它在最高级别,即基元或指针。不要将其与完全克隆和传递对象的深层传递值混淆。
dhackner

150
令人困惑的是,对象引用实际上是指针。一开始,SUN称它们为指针。然后市场营销人员得知“指针”是一个坏词。但是您仍然在NullPointerException中看到“正确的”命名法。
Falken教授的合同

3035

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

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的那个)被要求将他的名字改成Max
  • 在“ BBB”行
    • Dog创建一个新的。假设他在地址74
    • 我们将参数分配someDog给74
  • 在“ CCC”行
    • someDog跟随到Dog它所指向的位置(Dog地址为74 的对象)
    • Dog(地址74的那个)被要求将他的名字改成Rowlf
  • 然后,我们返回

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

myDog变化吗?

有钥匙

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

遵循完全正确地址并更改地址末尾是;但这不会更改变量。

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

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

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

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


164
这就是为什么常见的格言“ Java没有指针”如此令人误解的原因。
Beska

134
你错了,恕我直言。“记住myDog是一个指针,而不是实际的Dog,答案是否定的。myDog仍然具有值42;它仍指向原始的Dog。” myDog的值为42,但是其名称参数现在包含“ Max”,而不是// AAA行上的“ Rover”。
Özgür的

180
这样想吧。有人在名为“ annArborLocation”的纸条上有密歇根州安阿伯的地址(我的故乡,GO BLUE!)。您将其复制到一张称为“ myDestination”的纸上。您可以开车到“ myDestination”并种一棵树。您可能已经更改了该位置上的城市信息,但是并没有改变写在任何纸上的LAT / LON。您可以在“ myDestination”上更改LAT / LON,但不会更改“ annArborLocation”。有帮助吗?
Scott Stanchfield,

43
@Scott Stanchfield:大约一年前,我读了您的文章,它确实为我清除了一切。谢谢!我可以谦虚地建议一下:您应该提到,实际上有一个特定的术语描述了这种形式的“按值调用,其中值是参考”,这是Barbara Liskov发明的,用于描述其CLU语言的评估策略。 1974年,为了避免出现您的文章所引起的混淆,即通过共享进行调用(有时通过对象共享进行调用或仅通过对象进行调用),这几乎完美地描述了语义。
约尔格W¯¯米塔格

29
@Gevorg-C / C ++语言不拥有“指针”的概念。还有其他语言使用指针,但不允许使用C / C ++允许的相同类型的指针操作。Java有指针;他们只是免受恶作剧。
Scott Stanchfield's

1739

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. 声明一个名为ftype 的引用,Foo并为其分配一个Foo带有属性的type新对象"f"

    Foo f = new Foo("f");

    在此处输入图片说明

  2. 从方法方面,声明Foo具有名称的类型引用,a并对其进行初始分配null

    public static void changeReference(Foo a)

    在此处输入图片说明

  3. 调用方法时changeReferencea将为引用分配作为参数传递的对象。

    changeReference(f);

    在此处输入图片说明

  4. 声明一个名为btype 的引用,Foo并为其分配一个Foo带有属性的type新对象"b"

    Foo b = new Foo("b");

    在此处输入图片说明

  5. a = b使新的分配给参考a f,它的属性是对象"b"

    在此处输入图片说明

  6. 在调用modifyReference(Foo c)method时,将c创建一个引用并将对象分配给attribute "f"

    在此处输入图片说明

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

    在此处输入图片说明

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


82
+1好东西。好图。我还在这里找到了一个简洁的页面adp-gmbh.ch/php/pass_by_reference.html好,我承认它是用PHP编写的,但这是理解我认为很重要的区别(以及如何处理该区别的原则)。您的需求)。
DaveM

5
@ Eng.Fouad这是一个很好的解释,但是如果a指向与相同的对象f(并且从未获得f指向对象的自己的副本),则使用对该对象所做的任何更改也a应进行修改f(因为它们都在同一个对象上工作) ),因此在某个点上a必须获取自己指向的对象的副本f
0x6C38

14
@mrD当“ a”也指向同一对象“ f”时,则也可以通过“ f”观察到通过“ a”对该对象所做的任何更改,但未更改“ f”。'f'仍指向同一对象。您可以完全更改对象,但不能更改'f'指向的对象。这是一些人出于某些原因无法理解的基本问题。
Mike Braun 2013年

@MikeBraun ...什么?现在,您让我感到困惑:S。您刚刚写的内容与6.和7.显示的内容是否相反?
邪恶的洗衣机

6
这是我找到的最好的解释。顺便问一下,基本情况如何?例如,参数需要一个int类型,它是否仍将int变量的副本传递给参数?
allenwang '16

732

这将使您对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。
    • 这两个变量都具有引用的IDENTICAL COPIES,并且都引用相同的Person对象,Heap上的SAME对象和NOT COPY。

一张图片胜过千言万语:

价值传递

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

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

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

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

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

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


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


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


5
你的意思是指针?..如果我得到它的正确,在public void foo(Car car){ ... }car是本地的foo,它包含对象的堆的位置?因此,如果我将car的值更改为car = new Car(),它将指向堆上的不同Object?如果我通过更改car属性valu car.Color = "Red",则指向堆中的对象car将被修改。另外,在C#中也一样吗?请回复!谢谢!
dpp 2012年

11
@domanokz您正在杀了我,请不要再说这个词!;)请注意,我也可以回答这个问题而不必说“参考”。这是一个术语问题,'p'使情况更糟。我和苏格兰人对此有不同的看法。我想您已经了解了它在Java中的工作原理,现在您可以称其为通过值传递,按对象共享,按变量复制值,或者随时提出其他建议!只要您了解它的工作方式以及Object类型变量中的内容,我就不会在乎:只是一个邮政信箱地址!;)
Marsellus Wallace 2012年

9
听起来您刚刚通过了参考?我要鼓吹Java仍然是复制的按引用传递语言的事实。它是复制参考的事实不会改变术语。两个参考仍然指向相同的对象。这是一个纯粹主义者的论点……
John Strickler 2014年

3
那么,插入第9行理论线System.out.println(person.getName());会显示什么?“汤姆”还是“杰里”?这是最后一件事,将帮助我克服这种困惑。
TheBrenny 2014年

1
最终向我解释了“ 参考的价值”。
亚历山大·舒伯特

689

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

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

因此,在调用方法时

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

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

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


7
由于基元的值是不可变的(如String),因此这两种情况之间的区别并不是真正相关的。
圣保罗Ebermann

4
究竟。对于所有您可以通过可观察到的JVM行为看出的信息,原语可以通过引用传递并可以存在于堆中。他们没有,但是实际上无论如何都无法观察到。
重力

6
原语是不可变的?这是Java 7中的新功能吗?
user85421 2011年

4
指针是不可变的,原语通常是可变的。字符串也不是原始的,它是一个对象。而且,字符串的基础结构是可变数组。唯一不变的是长度,它是数组的固有特性。
kingfrito_5005

3
这是另一个指出该论点在很大程度上具有语义性质的答案。此答案中提供的引用定义将使Java“通过引用传递”。作者在最后一段中基本上承认了这一点,并指出它与“通过引用”“在实践中没有区别”。我怀疑OP之所以问是因为希望了解Java实现,而不是了解如何正确使用Java。如果在实践中没有区别,那么就没有任何关怀,甚至思考也将浪费时间。
Loduwijk

331

Java通过值传递引用。

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


27
但是,不断重复出现的“您不能更改在参数中传递的对象的值”的事情显然是错误的。您可能无法使它们引用另一个对象,但是您可以通过调用其方法来更改其内容。IMO意味着您将失去参考的所有好处,并且不会获得任何其他保证。
Timmmm 2011年

34
我从未说过“您不能更改在参数中传递的对象的值”。我会说“您不能更改作为方法参数传入的对象引用的值”,这是关于Java语言的真实声明。显然,您可以更改对象的状态(只要它不是不可变的)。
ScArcher2

20
请记住,您实际上无法在Java中传递对象;对象留在堆上。可以传递指向对象的指针(将其指针复制到被调用方法的堆栈框架中)。因此,您永远不会更改传入的值(指针),但可以自由地跟随它并在它指向的堆上进行更改。那就是传递价值。
Scott Stanchfield,2012年

10
听起来您刚刚通过了参考?我要鼓吹Java仍然是复制的按引用传递语言的事实。它是复制参考的事实不会改变术语。两个参考仍然指向相同的对象。这是一个纯粹主义者的论点……
John Strickler 2014年

3
Java不传递对象,而是将指针的值传递给该对象。这将在新变量中创建指向原始对象的该内存位置的新指针。如果您在方法中更改此指针变量的值(它指向的内存地址),则方法调用者中使用的原始指针将保持不变。如果将参数称为引用,则说明它是原始引用的副本,而不是原始引用本身,因此该对象现在有两个引用,这意味着它是按值传递的
theferrit32

238

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

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

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

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

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

结果:

堆栈上的基元

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

像这样:

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

ab * 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,因为第二个String的地址与第一个String相同,因此不会创建新的String,并且没有分配新的堆空间,但是在堆栈上创建了一个新的标识符。然后我们调用shout():创建一个新的堆栈框架并创建一个新的标识符,name并为其分配已经存在的String的地址。

拉达迪达达达达

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


7
但是,您应该跟进一个更复杂的示例,在该示例中,函数似乎将变量更改为其引用的地址。
布莱恩·彼得森

34
人们并不是在“围绕堆的真正问题跳舞”,因为那不是真正的问题。最好是实现细节,最坏情况是完全错误的。(对象很可能存在于堆栈中;谷歌“转义分析”。并且大量对象包含可能存在于堆栈中的基元。)真正的问题恰恰是引用类型和值类型之间的区别-特别是引用类型变量的值是引用,而不是引用的对象。
cHao

8
这是一个“实现细节”,因为从不需要Java才能真正向您显示对象在内存中的位置,并且实际上似乎决心避免泄漏该信息。它将对象放到堆栈上,您永远不会知道。如果您关心的话,那么您将专注于错误的事情,在这种情况下,这意味着忽略了真正的问题。
cHao 2013年

10
无论哪种方式,“基元都进入堆栈”都是不正确的。原始局部变量进入堆栈。(当然,如果尚未对其进行优化)。但是,局部引用变量也是如此。并且在对象内定义的原始成员无论对象位于何处都可以生存。
cHao 2013年

9
同意这里的评论。堆栈/堆是一个附带问题,并不相关。一些变量可能在堆栈上,一些变量在静态内存中(静态变量),而大量内存在堆上(所有对象成员变量)。这些变量中没有一个可以通过引用传递:从被调用的方法中,永远不可能更改作为参数传递的变量的值。因此,Java中没有按引用传递。
fishinear 2014年

195

为了显示对比,请比较以下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仅具有两种传递类型:内置类型按值传递,对象类型按指针值传递。


7
+1我还将添加Dog **objPtrPtr到C ++示例中,这样我们可以修改指针“指向”的内容。
Amro 2014年

1
但这不能回答Java中的给定问题。实际上,Java方法中的所有内容都是按值传递,仅此而已。
tauitdnmd

2
这个例子只是证明Java在C语言中使用的指针非常等效。C中的按值传递基本上是只读的?最后,这整个线程是难以理解的
amdev '19


166

基本上,重新分配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。之所以起作用,是因为bar是的值的副本,而该值baz只是对的引用"Hah!"。如果它是实际引用本身,则将foo重新定义baznull


8
我宁愿说bar是参考baz(或baz别名)的副本,它最初指向同一对象。
MaxZoom 2015年

String类和其他所有类之间是否没有什么区别?
Mehdi Karamosly

152

我不敢相信没有人提到过芭芭拉·里斯科夫(Barbara Liskov)。当她在1974年设计的CLU,她就遇到了这个相同的术语问题,她发明了长期的共享调用(也称为按对象共享呼叫通话的对象),用于调用”这个特定的情况下,由值,其中值参考”。


3
我喜欢这种命名方式。不幸的是,Java支持通过共享对象进行调用,但不支持按值调用(C ++也是如此)。Java仅支持对原始数据类型而不是复合数据类型的按值调用。
德里克·马哈

2
我真的不认为我们需要一个额外的术语-它只是针对特定类型的价值传递价值。添加“按原语调用”会增加任何说明吗?
Scott Stanchfield 2010年


因此,我可以通过共享一些全局上下文对象甚至通过包含其他引用的上下文对象来传递引用?我仍然按值传递,但至少可以访问我可以修改的引用并使它们指向其他内容。
YoYo

1
@Sanjeev:按对象共享调用是传递值的一种特殊情况。但是,很多人都激烈地认为Java(以及类似的语言,如Python,Ruby,ECMAScript,Smalltalk)是按引用传递的。我也更喜欢称其为“按值传递”,但是“按对象共享”似乎是一个合理的术语,即使那些认为它不是按值传递的人也可以同意。
约尔格W¯¯米塔格

119

的问题的关键是,字参考在表达手段的东西从字的通常含义完全不同的“按引用传递” 参考在Java中。

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


7
@Gevorg-那么“ NullPointerException”是什么?
2013年

5
@Hot:一个不幸的命名异常,发生在Java决定使用明确术语之前。在c#中,语义上等效的异常称为NullReferenceException。
JacquesB 2013年

4
在我看来,在Java术语中始终使用“引用”是一种妨碍理解的现象。
热门点击2013年

我学会了将这些所谓的“对象的指针”称为“对象的句柄”。这减少了歧义。
moronkreacionz 2015年

86

在Java中,所有内容都是参考,因此当您遇到类似情况时: Point pnt1 = new Point(0,0); Java会执行以下操作:

  1. 创建新的Point对象
  2. 创建新的Point引用,并将该引用初始化先前创建的Point对象上的point(引用)
  3. 从这里开始,通过Point对象生命,您将通过pnt1引用访问该对象。因此可以说,在Java中,您可以通过引用来操纵对象。

在此处输入图片说明

Java不会通过引用传递方法参数。它通过价值传递它们。我将使用此站点的示例:

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

在这条线上,“传递价值”成为了舞台。

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

参考文献pnt1pnt2按值传递给棘手的方法,现在你引用其手段pnt1pnt2有自己的copies命名arg1arg2。所以pnt1,并arg1 到同一个对象。(对于pnt2arg2在此处输入图片说明

tricky方法中:

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

在此处输入图片说明

接下来的tricky方法

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

在这里,您首先创建新的temp点参考,该点将指向arg1参考相同的位置。然后,您将参考arg1移至指向arg2参考相同的位置。最后arg2指向一样的地方temp

在此处输入图片说明

从这里的范围tricky方法走了,你没有获得任何更多的引用:arg1arg2temp但是重要的一点是,当这些引用“存在”时,您所做的一切都会永久性地影响它们所指向的对象。

因此,在执行method之后tricky,当您返回时main,您将遇到以下情况: 在此处输入图片说明

因此,现在,完全执行程序将是:

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

7
在Java中,当您说“在Java中,所有内容都是引用”时,表示所有对象都是通过引用传递的。 原始数据类型不通过引用传递。
埃里克

我可以在主方法中打印交换的值。在trickey方法中,添加以下语句arg1.x = 1; arg1.y = 1; arg2.x = 2; arg2.y = 2;,例如arg1现在持有pnt2引用,而arg2现在持有pnt1引用,因此,其打印X1: 2 Y1: 2 X2: 1 Y2: 1
Shahid Ghafoor

85

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传递给函数时,它不会直接传递对象测试的实际参考值,而是会创建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,如下所示:

第三个插图

希望这会有所帮助。


71

无论使用哪种语言,引用在表示时始终是一个值。

进入框视图的外部,让我们看一下Assembly或一些低级的内存管理。在CPU级别,如果将任何内容写入内存或CPU寄存器之一,则对任何内容的引用都会立即成为。(这就是为什么指针是一个很好的定义。它是一个值,同时具有目的)。

内存中的数据有一个位置,在该位置有一个值(字节,字或其他内容)。在组装中,我们有一个方便的解决方案来给特定位置命名(又名变量),但是在编译代码时,汇编程序将简单地替换Name为指定的位置,就像您的浏览器用IP地址替换域名一样。

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

比方说,我们有一个变量富,它的位置是在第47字节内存,其价值是5,我们有另一个变量Ref2Foo这是在第223次内存字节,并且它的值是47.这Ref2Foo可能是一个技术性的变量,不是由程序明确创建的。如果仅查看5和47而不显示任何其他信息,则只会看到两个Values。如果您将它们用作参考,则必须前往5我们:

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

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

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

  1. 5被复制到CPU寄存器之一(即EAX)。
  2. 5被压入堆栈。
  3. 47被复制到CPU寄存器之一
  4. 47推入堆栈。
  5. 223被复制到CPU寄存器之一。
  6. 223被压入堆栈。

在上述每种情况下,都会创建一个值- 现有值的副本 -现在由接收方法来处理它。当您在方法中编写“ Foo”时,它是从EAX中读出的,还是被自动 取消引用或双重取消引用的,所以过程取决于语言的工作方式和/或Foo的类型。这对开发人员是隐藏的,直到她规避了取消引用过程。所以参考就是一个表示时,因为引用是必须处理的值(在语言级别)。

现在我们将Foo传递给该方法:

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

忽略细节,即使是通过引用传递的语言也会将值传递给函数,但是那些函数知道必须将其用于解引用目的。这种“按值传递引用”对程序员而言只是隐藏的,因为它实际上是无用的,并且术语仅是“ 按引用传递”

严格的值传递也是没有用的,这意味着每次调用以数组为参数的方法时,都必须复制100 MB的数组,因此Java不能严格地值传递。每种语言都会传递对该巨大数组的引用(作为值),并且如果该数组可以在方法内部进行本地更改,或者采用该方法(如Java),则可以采用写时复制机制; (调用者的视图)和几种语言可以修改引用本身的值。

因此,在短期和Java的自己的术语,Java是传址值,其中可以是:无论是实值是一个的表示参考


1
在具有通过引用的语言中,通过的事物(引用)是短暂的。收件人不是“应该”复制它。在Java中,传递数组是一个“对象标识符”,相当于当构造的第24601个对象是数组时,纸条上写着“ Object#24601”。接收者可以将“对象#24601”复制到任何所需的位置,并且只要有纸条说“对象#24601”,任何人都可以使用任何数组元素执行任何操作。当然,传递的位模式实际上不会说“对象#24601”,但是……
supercat 2014年

...关键是识别数组的object-id的接收者可以在任何需要的地方存储该对象id并将其提供给想要的任何人,任何接收者都可以在任何时候访问或修改该数组想要。相比之下,如果数组通过支持Pascal这样的语言的引用传递给Pascal,则被调用的方法可以对数组做任何想做的事情,但是不能以允许代码修改数组的方式存储引用它返回后。
2014年

事实上,在帕斯卡尔你可以得到每个变量的地址,无论是通过按引用或本地复制,addr做它非类型化,@类型做它,你可以在以后修改引用变量(除了本地复制的)。但是我不明白你为什么要这么做。您的示例(对象#24601)中的那条纸条仅供参考,其目的是帮助在内存中查找数组,它本身不包含任何数组数据。如果重新启动程序,则即使其内容与上一次运行相同,相同的数组也可能会获得不同的object-id。
karatedog 2014年

我以为“ @”运算符不是标准Pascal的一部分,而是作为通用扩展实现的。它是标准的一部分吗?我的观点是,在具有真正的通过引用且无法构造指向短暂对象的非暂时性指针的语言中,代码在传递数组通过参考文献可以知道,除非收件人“作弊”,否则以后仍将保留唯一的参考文献。在Java中完成此操作的唯一安全方法是构造一个临时对象...
supercat 2014年

...封装了一个AtomicReference,既不公开引用也不公开其目标,而是包括对目标执行操作的方法;一旦将对象传递给的代码返回,AtomicReference[其创建者直接引用的对象]应该无效并放弃。那会提供适当的语义,但是会很慢而且很棘手。
supercat 2014年

70

Java是按值调用

怎么运行的

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

  • 如果它是原始数据类型,则这些位包含原始数据类型本身的值,这就是为什么如果我们在方法内部更改标头的值,则它不会反映外部的更改。

  • 如果它是像Foo foo = new Foo()这样的对象数据类型,则在这种情况下,对象地址的副本通过文件shortit传递,假设我们在C:\ desktop处有一个文本文件abc.txt并假设我们将相同的文件,并将其放在C:\ desktop \ abc-shortcut中,因此当您从C:\ desktop \ abc.txt访问文件并写入“ Stack Overflow”并关闭文件,然后再次从快捷方式打开文件时,写“是最大的供程序员学习的在线社区”,那么文件总更改将为“堆栈溢出是最大的供程序员学习的在线社区”意味着每次打开同一文件都从这里打开文件无关紧要,在这里我们可以假设将Foo作为文件,并假定将foo存储在123hd7h(原始地址,例如C:\ desktop \ abc.txt)和234jdid(复制的地址,例如C:\ desktop \ abc-shortcut,实际上包含文件的原始地址)中。因此为了更好的理解,使快捷方式文件和感觉变得更好。


66

已经有很好的答案可以解决这个问题。我想通过分享一个非常简单的示例(将进行编译)做出一点贡献,以比较 c ++中的“按引用传递”和Java中的“按值传递”之间的行为。

几点:

  1. 术语“引用”是具有两个单独含义的重载。在Java中,它只是表示一个指针,但是在“按引用传递”的上下文中,它表示传入的原始变量的句柄。
  2. Java是按值传递。Java是C(在其他语言中)的后代。在C之前,FORTRAN和COBOL等几种(但不是全部)较早的语言支持PBR,但C不支持。PBR允许这些其他语言更改子例程中传递的变量。为了完成同样的事情(即更改函数内部变量的值),C程序员将指向变量的指针传递给函数。受C启发的语言(例如Java)借鉴了这一思想,并像C一样继续将指针传递给方法,只是Java调用了其指针References。同样,这是“参考”一词与“通过参考”中的不同用法。
  3. C ++通过使用“&”字符(在C和C ++中恰好与用来表示“变量的地址”的字符相同)声明引用参数,从而允许通过引用。例如,如果我们按引用传递指针,则参数和参数不仅指向同一对象。相反,它们是相同的变量。如果将一个设置为其他地址或设置为null,则另一个设置也将设置为null。
  4. 在下面的C ++示例中,我通过引用指针传递给以 null结尾的字符串。在下面的Java示例中,我按值传递了对String的Java引用(再次与指向String的指针相同)。注意注释中的输出。

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().
     }
}

编辑

一些人发表了评论,似乎表明他们不是在看我的例子,还是没有得到c ++例子。不知道断开连接在哪里,但是不清楚c ++示例。我在pascal中发布了相同的示例,因为我认为通过引用传递在pascal中看起来更干净,但是我可能是错的。我可能只会让人们更加困惑;我希望不是。

在pascal中,通过引用传递的参数称为“ var参数”。在下面的过程setToNil中,请注意在参数“ ptr”之前的关键字“ var”。当指针传递给该过程时,它将通过reference传递。请注意行为:当此过程将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 Programming Language”摘录,第2章,第2.6.5节

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

他继续就物体提出了相同的观点。。。

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

在同一部分的末尾,他对Java仅通过值传递而从不传递引用进行了更广泛的说明。

Java编程语言不会通过引用传递对象。它 按值传递对象引用。由于相同引用的两个副本引用了相同的实际对象,因此通过一个引用变量进行的更改将通过另一个变量可见。仅有一个参数传递模式- 按值传递- 有助于简化操作。

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

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

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

编辑3

我可能对这个职位有些痴迷。可能是因为我觉得Java的制造商无意间散布了错误信息。如果他们没有使用“引用”一词作为指针,而是使用了其他东西(例如dingleberry),那就没有问题了。您可能会说:“ Java通过值而不是通过引用传递dingleberries”,并且不会有人感到困惑。

这就是只有Java开发人员对此有疑问的原因。他们看着“指称”一词,以为自己确切地知道那是什么意思,因此他们甚至不必理会相反的论点。

无论如何,我在一个较旧的帖子中注意到了一条评论,该评论使我非常喜欢一个气球类比。如此之多,以至于我决定将一些剪贴画粘合在一起,制作出一系列动画片来说明这一点。

按值传递引用 -引用的更改不会反映在调用者的作用域中,但对象的更改会反映在调用者的作用域中。这是因为引用已复制,但是原始副本和副本都引用同一对象。 按值传递对象引用

通过引用传递-没有引用的副本。调用者和被调用函数都共享单个引用。对引用或对象数据的任何更改都会反映在调用者的作用域中。 通过参考

编辑4

我看过有关此主题的文章,这些文章描述了Java中参数传递的低级实现,我认为这很好并且非常有帮助,因为它使抽象概念具体化。但是,对我来说,问题更多的是语言规范中描述的行为,而不是行为的技术实现。这是Java语言规范第8.4.1节的摘录:

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

这意味着,java在执行方法之前创建了传递的参数的副本。就像谁在大学学习的编译器大多数人来说,我用“龙书”编译器的书。在第1章中对“按值调用”和“按引用调用”有很好的描述。“按值调用”描述与Java规范完全匹配。

当我在90年代研究编译器时,我使用了1986年的第一版书,该书比Java早9到10年。但是,我遇到了2007年第二版的副本,其中实际上提到了Java!标有“参数传递机制”的第1.6.6节很好地描述了参数传递。以下是“按值调用”标题下的摘录,其中提到了Java:

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


4
老实说,您可以通过说Java仅针对原始类型按值传递来简化此答案。从Object继承的所有内容均有效地通过引用传递,其中引用是您要传递的指针。
水肺史蒂夫,

1
@JuanMendes,我只是以C ++为例;我可以用其他语言给你例子。术语“通过引用传递”早在C ++出现之前就已经存在。这是一个教科书术语,定义非常明确。而且根据定义,Java不是通过引用传递的。您可以根据需要继续使用该术语,但用法与教科书定义不一致。它不仅是指向指针的指针。这是语言提供的一种构造,允许“通过引用传递”。请仔细看一下我的例子,但请不要相信我的话,请自己检查一下。
桑耶夫

2
@AutomatedMike我认为将Java描述为“按引用传递”也是一种误导,它们的引用不过是指针。正如Sanjeev所写的价值一样,无论Java创建者使用什么,引用和指针都是教科书术语,具有各自的含义。
JędrzejDudkiewicz

1
@ArtanisZeratul的观点很微妙,但并不复杂。请看一下我发布的动画片。我觉得这很简单。Java是按价值传递的不只是一种观点。教科书中对传递值的定义是正确的。另外,“总是说通过价值传递的人”包括像Java的创建者James Gosling这样的计算机科学家。请在我的帖子中查看他的书中“编辑2”下的引用。
桑耶夫

1
在Java(和JS等其他语言)中不存在“按引用传递”的好答案。
newfolder

56

据我所知,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这样的不可变对象。


交换测试+1 –可能是区分按引用传递按值传递引用的最直接,最相关的方法。如果您可以轻松编写一个函数swap(a, b),该函数可以(1)从调用者的POV进行交换ab从调用者的POV进行交换,则(2)在静态类型允许的范围内是与类型无关的(这意味着与其他类型一起使用只需要更改aand 的声明类型b)。 ,而(3)不需要调用者显式传递指针或名称,则该语言支持按引用传递。
cHao 2013年

“对于原始数据类型,您将使用一个副本;对于对象,您将使用对对象的引用的一个副本” –完美地编写了!

55

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

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

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


52

让我尝试通过四个示例来解释我的理解。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:

/ **这种“按价值传递”具有“按引用传递”的感觉

有人说原始类型和“字符串”是“按值传递”,而对象是“按引用传递”。

但是从这个例子中,我们可以理解它实际上只是按值传递,请记住,这里我们将引用作为值传递。即:引用按值传递。这就是为什么能够更改并且在本地范围后仍然适用的原因。但是我们不能在原始范围之外更改实际参考。下一个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]

49

在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中不可用。


48

我以为我可以提供此答案,以便在规格中添加更多详细信息。

首先,按引用传递与按值传递有什么区别?

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

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

或来自维基百科,关于通过引用的主题

在按引用调用评估(也称为按引用传递)中,函数接收对用作参数的变量的隐式引用,而不是其值的副本。这通常意味着该函数可以修改(即分配给)用作参数的变量,该变量将被其调用者看到。

对通过噪声值的主题

在按值调用中,对参数表达式进行求值,并将结果值绑定到函数中的相应变量。如果函数或过程能够为其参数分配值,则仅分配其本地副本。

其次,我们需要知道Java在其方法调用中使用了什么。在Java语言规范状态

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

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

该论点的价值是什么?

让我们考虑引用类型,Java虚拟机规范指出

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

Java语言规范还规定

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

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

所以

public void method (String param) {}
...
String var = new String("ref");
method(var);
method(var.toString());
method(new String("ref"));

全部将String实例的引用值绑定到方法的新创建参数param。这正是值传递定义的描述。因此,Java是按值传递

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

这通常意味着该函数可以修改(即分配给)用作参数的变量,该变量将被其调用者看到。

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


原始值也在Java虚拟机规范(此处)中定义。类型的值是相应的整数或浮点值,已正确编码(8、16、32、64等位)。


42

在Java中,仅传递引用,并按值传递:

Java参数全部按值传递(方法使用该引用时,该引用将被复制):

对于基本类型,Java行为很简单:将值复制到基本类型的另一个实例中。

对于对象,这是相同的:对象变量是仅包含使用“ new”关键字创建的对象地址的指针(存储桶),并且像原始类型一样被复制。

该行为可能与基本类型有所不同:由于复制的对象变量包含相同的地址(指向相同的对象)。对象的内容/成员可能仍然可以在方法中进行修改,然后在外部进行访问,从而产生一种幻想,即(包含的)对象本身是通过引用传递的。

“字符串”对象似乎是城市传奇的一个很好的反例,它说“对象通过引用传递”:

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

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


那么关于对象的是byRef,关于基元的是byVal?
Mox

@mox请阅读:对象不是通过引用传递的,这是一个分类账:String a = new String(“ unchanged”);
user1767316

1
@Aaron“通过引用传递”并不意味着“传递在Java中称为“引用”的类型的成员的值”。“引用”的两种用法具有不同的含义。
philipxy

2
相当令人困惑的答案。之所以无法更改通过引用传递的方法中的字符串对象,是因为String对象在设计上是不可变的,并且您无法执行strParam.setChar ( i, newValue )。就是说,字符串和其他任何东西都是按值传递的,并且由于String是非原始类型,因此该值是对用new创建的对象的引用,您可以使用String.intern()进行检查。 。
zakmck

1
相反,您不能通过param =“ another string”更改字符串(等效于new String(“ another string”)),因为param的引用值(现在指向“ another string”)无法从方法的返回身体。但这对于任何其他对象都是如此,不同之处在于,当类接口允许它时,您可以执行param.changeMe(),并且尽管param的引用值本身,但由于param指向该对象,因此被引用的顶级对象将发生变化。 (以C表示的寻址位置)无法从该方法弹出。
zakmck

39

这种区别,或者也许只是我以前记得与原始海报相同的印象时的方式是:Java总是通过价值传递。Java中的所有对象(在Java中,除基元以外的所有对象)都是引用。这些引用按值传递。


2
我发现倒数第二句很容易让人误解。“ Java中的所有对象都是引用”并不是真的。只是对那些对象的引用才是引用。
达伍德·伊本·卡里姆

37

正如许多人之前提到的,Java始终是按价值传递的

这是另一个示例,可以帮助您了解不同之处(经典的swap示例):

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的引用只会在本地范围内更改,而不会在此方法之外更改。


32

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

4
这是查看Java按值传递的最清晰,最简单的方法。objnull)的值传递给init,而不是对的引用obj
David Schwartz

31

我一直认为它是“通过副本”。它是值的副本,可以是原始值或引用。如果它是原语,则是作为值的位的副本;如果它是Object,则是引用的副本。

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的输出:

名称= Maxx
名称= Fido

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


5
“按值传递”是按值传递的意思
TJ Crowder

@TJCrowder。“按副本传递”可能是出于Java目的表达相同概念的一种更合适的方式:因为从语义上讲,这很难使人联想到类似于通过引用传递的想法,这是Java中想要的。
mike啮齿动物

@mikerodent-抱歉,我没有遵守。:-)“按值传递”和“按引用传递”是这些概念的正确术语。我们应该使用它们(在人们需要它们时提供它们的定义),而不是编造新的定义。上面我说过“按副本传递”是“按值传递”的意思,但是实际上,不清楚“按副本传递”是什么意思-副本是什么?价值?(例如,对象引用。)对象本身?因此,我坚持使用艺术术语。:-)
TJ Crowder

28

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

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

当涉及对象时,对象本身不能传递给方法。因此,我们传递了对象的引用(地址)。我们可以使用此引用来操纵原始对象。

Java如何创建和存储对象:创建对象时,我们将对象的地址存储在引用变量中。让我们分析以下语句。

Account account1 = new Account();

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

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

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

在下面的图像中,您可以看到main方法中有两个参考变量(在C / C ++中将它们称为指针,并且我认为该术语使理解此功能更加容易。)基本变量和参考变量保存在堆栈存储器中(下图左侧)。array1和array2引用变量“点”(如C / C ++程序员所称)或分别引用a和b数组,它们是堆内存中的对象(这些引用变量所保存的值是对象的地址)(下图的右侧) 。

按值传递示例1

如果我们将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);
    }
}

按值传递示例2

所以,如果我们说

array1[0] = 5;

在reverseArray方法中,它将更改数组a。

我们在reverseArray方法(数组2)中还有另一个引用变量,它指向数组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数组。没有引用变量指向对象(数组)因此,它符合垃圾收集的条件。

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


27

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

还提到了Java。这是简短的摘要:

  • Java按值传递参数
  • “按值”是Java中将参数传递给方法的唯一方法
  • 使用给定对象的方法作为参数将改变对象,因为引用指向原始对象。(如果该方法本身更改了某些值)

27

简而言之,Java对象具有一些非常特殊的属性。

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

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

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


我认为这与我对Java对象及其参考的理解非常接近。Java中的对象通过引用副本(或别名)传递给方法。
MaxZoom 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.