【问题标题】:Altering passed parameters in Java (call by ref,value)在 Java 中更改传递的参数(通过 ref、value 调用)
【发布时间】:2010-07-02 11:47:31
【问题描述】:

我实际上认为我对 Java 中传递值的实际工作方式有一个很好的了解,因为那是我通过的 SCJP 证书的一部分。直到今天,我在工作中发现了这样一种方法:

public void toCommand(Stringbuffer buf) {

  buf.append("blablabla");
}

然后该方法的调用者使用这样的函数:

StringBuffer buf = new StringBuffer();
toCommand(buf);
String str = buf.toString();

现在我认为该代码会给 str 值“”,但实际上它给它的值来自方法。这怎么可能?我认为在 Java 中事情不是这样工作的?

无论哪种方式...用Java编写这样的代码应该被认为是一种不好的做法,对吧?因为我可以想象它会给它带来一些混乱。

我实际上花了一些时间搜索这个,但我对这些消息来源所说的解释是它不应该工作。我错过了什么?

http://www.yoda.arachsys.com/java/passing.html
http://javadude.com/articles/passbyvalue.htm

塞巴斯蒂安

【问题讨论】:

  • 你应该在 SCJP 上要求退还你的钱——我相信他们应该教你这个! :-)
  • @mikera +1 这是语言的中心点之一,将对象作为参数传递,我很惊讶他们错过了它。
  • @Taisin:我同意。我从来没有关注过认证,但这只是普通的sad
  • 我很确定他们倾向于使用 String (immutable) 和 Object 类来解决他们的问题。对象没有任何方法可以改变对象的任何内容,所以这可能就是我错过这个细节的原因。我很确定在准备 SCJP 或实际测试时,我从来没有通过这些类型的问题。无论如何,我宁愿在方法中创建缓冲区并返回 buffer.toString,而不是发送 StringBuffer 作为参数。但也许这并不是在所有情况下都是最佳的,比如在组装电子邮件时,正如有人提议的那样。
  • @sebastianlarsson 实际上,您必须在认证后提出这样的问题这一事实说明了很多认证价值。问题是,这不是细节,而是语言的关键点。令人惊讶的是,他们在问题中错过了它。顺便说一句,当文本操作被分成几种方法时,将 StringBuffer 作为参数发送是一个很好的做法——如果你,f.ex,通过许多对象、继承等组合状态报告。节省内存。字符串操作很繁重。

标签: java parameter-passing


【解决方案1】:

Java 是按值传递的。对象引用的是对象引用,而不是对象本身。因此,toCommand 方法接收到值的副本,它是对对象的引用——调用者正在引用的同一对象。

这与从两个变量引用对象时完全相同:

StringBuffer buf1;
StringBuffer buf2;

buf1 = new StringBuffer();
buf2 = buf1; // Still ONE object; there are two references to it
buf1.append("Hi there");   
System.out.println(buf2.toString()); // "Hi there"

无偿的 ASCII 艺术:

 +--------------------+
    buf1--------->| |
                  | === 对象 === |
                  | |
    buf2---------->|数据:|
                  | * foo = "酒吧" |
                  | * x = 27 |
                  | |
                  +--------------------+

另一种思考方式是 JVM 有一个由 ID 索引的所有对象的主列表。我们创建一个对象 (buf1 = new StringBuffer();),JVM 为该对象分配 ID 42 并将该 ID 存储在 buf1 中。每当我们使用buf1 时,JVM 都会从中获取值 42 并在其主列表中查找该对象,并使用该对象。当我们执行buf2 = buf1; 时,变量buf2 获得值42 的副本,因此当我们使用buf2 时,JVM 看到对象引用#42 并使用同一个对象。这不是字面上的解释(尽管从平流层的角度来看,如果您将“JVM”读作“JVM 和内存管理器和操作系统”,它不是一百万英里),但有助于思考对象引用实际上是什么。

在此背景下,您可以看到 toCommand 如何获得 reference(42 或其他),而不是实际的 StringBuffer 对象数据。所以对它的操作在主列表中查找它并改变它的状态(因为它保存状态信息并允许我们改变它)。调用者看到对象状态的变化,因为 object 持有状态,引用只是指向对象。

不管怎样……用Java编写这样的代码应该被认为是一种不好的做法,对吧?

完全没有,这是正常的做法。如果不这样做,将很难使用 Java(或大多数其他 OOP 语言)。与intlong 等基元相比,对象很大,因此移动它们的成本很高;对象引用是基元的大小,因此它们很容易传递。此外,拥有事物的副本会使系统的各个部分难以交互。拥有对共享对象的引用使其变得非常容易。

【讨论】:

  • 我发布的一篇文章的作者说,由于它是被复制的引用而不是实际的对象,因此性能不是问题。
  • @sebastianlarsson:确实,您移动的数据量越小,事情就会越快。我不会称其为 中心 点,但它确实如此。
  • 很好的例子。我已经编辑了代码,以便它设置buf2 = buf1 之前 它附加到buf1。我希望这没问题,它可以更明确地说明正在发生的事情。即buf2 不是buf1 对象的副本。
  • @mikej:谢谢你这样做。起初我并不确定,但我越想它,它就越聪明。 :-)
【解决方案2】:

StringBuffer可变的toCommand() 方法获取一个 reference 到该对象的 按值传递(引用),该引用允许该方法更改可变的 StringBuffer。 p>

如果你在想为什么我们不能用String 来做这件事,那是因为String不可变的 在这种情况下它会导致创建另一个String 对象 并且没有在传递引用的对象中反映更改。

而且我不明白为什么这是不好的做法。

【讨论】:

    【解决方案3】:

    这是因为在方法中,当您传递一个对象时,该对象的 referencecopy 是按值传递的。考虑下面的例子:

        public class Test {
    
         public static void modifyBuff(StringBuffer b)   {
            b.append("foo");
         }
    
         public static void tryToNullifyBuff(StringBuffer b)   {
            b = null; // this will not affect the original reference since 
                      // the once passed (by value) is a copy
         }
    
    
         public static void main(String[] args) {
            StringBuffer buff = new StringBuffer();  // buff is a reference 
                                                     // to StringBuffer object
    
            modifyBuff(buff);
    
            System.out.println(buff); // will print "foo"
    
            tryToNullifyBuff(buff); // this has no effect on the original reference 'buff'
    
            System.out.println(buff); // will still print "foo" because a copy of 
                                      // reference buff is passed to tryToNullifyBuff() 
                                      // which is made to reference null 
                                     // inside the method leaving the 'buff' reference intact
         }
    }
    

    这可以通过其他可变对象来完成,例如 Collection 类。这是 这并不是一个坏习惯,事实上某些设计积极地使用了这种模式。

    【讨论】:

      【解决方案4】:

      str 的值为 "blablabla",因为对 StringBuilder 实例的 reference 被传递给 toCommand()

      这里只创建了一个 StringBuffer 实例 - 你将它的引用传递给 toCommand() 方法。因此,在toCommand() 方法中对StringBuffer 调用的任何方法都会在调用方法中StringBuffer 的同一实例上调用。

      【讨论】:

        【解决方案5】:

        这并不总是坏习惯。考虑组装电子邮件的任务,然后您可以执行以下操作:

         StringBuilder emailBuilder = new StringBuilder();
         createHeader(emailBuilder);
         createBody(emailBuilder);
         createFooter(emailBuilder);
         sendEmail(emailBuilder.toString());
        

        它肯定会被用来制造混乱,对于公共 API,如果传递的引用的值发生了变化,应该在 javadoc 中添加一两个注释。

        Java API 的另一个突出示例:

        Collections.sort(list);
        

        正如其他已经解释过的,只是为了完成答案:StringBuffer 的引用值(toCommand,所以在您访问的toCommand 方法的外部和内部同一个 StringBuffer 实例。

        【讨论】:

        • 我注意到的代码实际上是在一个使用线程的项目中,所以实际上可能有他们使用 StringBuffer 的原因。
        • 可能是 - 或者它最初是为 Java 1.4.2 或更低版本编写的,因为 StringBuilder 是随 Java 1.5 引入的
        【解决方案6】:

        由于您获得了对实例的引用,因此您可以调用其上的所有方法,但不能将其分配给其他任何东西。

        public void toCommand(Stringbuffer buf) {
            buf.append("blablabla"); // okay
            buf = new Stringbuffer(); // no "effect" outside this method
        }
        

        【讨论】:

        • 这就是我认为无法在方法中更改对象的原因。在该方法中创建新的 StringBuffer 只会更改对象“buf”(对原始对象的引用的副本)所指的对象。谢谢,现在我明白了!
        • @sebastianlarsson:没错,你明白了
        【解决方案7】:

        当您将对象传递给方法时,与 C++ 不同,只有对该对象的引用会被复制。因此,当将可变对象传递给方法时,您可以对其进行更改,并且更改会反映在调用方法中。 如果您对 C/C++ 编程过于深入,您应该知道在 java 中,按引用传递(在 C++ 术语中)是默认(也是唯一)将参数传递给方法的方式。

        【讨论】:

          【解决方案8】:

          只是为了回应一些关于 SCJP 的 cmets(抱歉,没有权限离开 cmets - 如果留下答案不是正确的做法,我们深表歉意)。

          在 SCJP 考试中,将对象引用作为方法参数传递,不可变 String 对象和 StringBuffer / StringBuilder 对象之间的差异是考试的一部分 - 请参阅此处的第 3.1 和 7.3 节:

          http://www.javadeveloper.co.in/scjp/scjp-exam-objectives.html

          Kathy Sierra / Bert Bates 考试学习指南(事实上是官方的考试学习指南)广泛涵盖了这两个主题。

          【讨论】:

            猜你喜欢
            • 2013-12-31
            • 2020-06-08
            • 2011-04-29
            • 2016-12-07
            • 2014-03-19
            • 1970-01-01
            • 2011-02-03
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多