【问题标题】:Can someone explain to me what the reasoning behind passing by "value" and not by "reference" in Java is?有人可以向我解释在 Java 中通过“值”而不是“引用”传递的原因是什么?
【发布时间】:2010-10-05 14:17:26
【问题描述】:

我对 Java 还很陌生(多年来一直在写其他东西),除非我遗漏了什么(我很高兴在这里错了),否则以下是一个致命的缺陷......

String foo = new String();
thisDoesntWork(foo);
System.out.println(foo);//this prints nothing

public static void thisDoesntWork(String foo){
   foo = "howdy";
}

现在,我很清楚(措辞相当糟糕的)概念,即在 java 中,所有内容都是通过“值”而不是“引用”传递的,但是 String 是一个对象,并且有各种花里胡哨,所以,人们会期望,与 int 不同,用户将能够对传递给方法的内容进行操作(并且不会被重载 = 设置的值所困扰)。

有人可以向我解释这种设计选择背后的原因是什么吗?正如我所说,我不想待在这里,也许我错过了一些明显的东西?

【问题讨论】:

  • 我不太确定你在这里想说什么。请澄清一下?
  • 上一个标题不好,因为按标题搜索你问题的内容是不可能的。
  • 没问题,这个标题更有意义:)
  • 把java引用变量想象成指针,一切都会清楚;)

标签: java pass-by-reference pass-by-value


【解决方案1】:

This rant 解释得比我想象的还要好:

在 Java 中,原语是按值传递的。但是,对象不是 通过引用传递。一个正确的说法是对象引用 按值传递。

【讨论】:

  • 这个链接很好,我认为这应该是最佳答案。
【解决方案2】:

当您传递“foo”时,您将 reference 传递给“foo”作为 ThisDoesntWork() 的值。这意味着当您在方法内部对“foo”进行赋值时,您只是将局部变量 (foo) 的引用设置为对新字符串的引用。

在考虑字符串在 Java 中的行为方式时要记住的另一件事是字符串是不可变的。它在 C# 中的工作方式相同,并且有一些充分的理由:

  • 安全性:如果没有人可以修改它,没有人可以将数据塞入您的字符串并导致缓冲区溢出错误!
  • 速度:如果您可以确定您的字符串是不可变的,那么您就知道它的大小始终相同,并且您在操作它时不必移动内存中的数据结构。您(语言设计者)也不必担心将 String 实现为慢速链表。不过,这对双方都有好处。仅使用 + 运算符附加字符串可能会占用大量内存,您必须使用 StringBuilder 对象以高性能、内存高效的方式执行此操作。

现在谈谈你更大的问题。为什么对象以这种方式传递?好吧,如果 Java 将您的字符串作为您传统上称为“按值”的方式传递,那么它必须在将整个字符串传递给您的函数之前实际复制整个字符串。这很慢。如果它通过引用传递字符串并让您更改它(就像 C 一样),您就会遇到我刚刚列出的问题。

【讨论】:

  • 这实际上与字符串不可变这一事实无关。如果他通过了一个 List,同样的事情也会发生,而且 list 是可变的
  • 手头的问题与字符串的不变性无关。该行为与任何其他对象完全相同。我不确定 String 的不变性与安全性有多大关系,我认为这更像是一个性能问题。
  • 实际上,在 Java 中按值传递参数的行为将完全相同。或者实际上无论如何。他展示的行为只与参数绑定有关。
  • @Axelle:您缺少“通过引用传递”的含义。 Java确实按值传递参数。如果它具有传递引用,则效果将非常不同。您需要了解传递引用的真正含义。它与“按值传递引用”不同。
  • 让我们把java引用想象成C指针。一切都变得如此简单:要修改参数,你只需做那个人所做的事情(foo = "howdy")。要修改指向的对象(如果有的话),如果对象不是不可变的,则使用“add”之类的东西。
【解决方案3】:

因为我最初的答案是“为什么会发生”而不是“为什么语言设计成这样”,所以我会再试一次。

为了简单起见,我将去掉方法调用并以另一种方式显示正在发生的事情。

String a = "hello";
String b = a;
String b = "howdy"

System.out.print(a) //prints hello

要让最后一条语句打印“hello”,b 必须指向与 a 指向的内存中相同的“洞”(指针)。当您想要通过引用传递时,这就是您想要的。 Java 决定不朝这个方向发展的原因有两个:

  • 指针令人困惑 Java 的设计者试图删除其他语言中一些更令人困惑的东西。指针是 C/C++ 以及运算符重载中最容易被误解和使用不当的构造之一。

  • 指针存在安全风险 指针在误用时会导致许多安全问题。恶意程序将某些内容分配给该部分内存,然后您认为是您的对象实际上是其他人的。 (Java 已经摆脱了最大的安全问题,缓冲区溢出,检查数组)

  • 抽象泄漏当您开始准确地处理“内存中的内容和位置”时,您的抽象就变得不那么抽象了。虽然抽象泄漏几乎肯定会渗入一种语言,但设计人员并不想直接将其融入其中。

  • 你只关心对象 在 Java 中,一切都是对象,而不是对象占用的空间。但是,添加指针会使对象占据的空间变得很重要......

您可以通过创建“孔”对象来模拟您想要的。您甚至可以使用泛型使其类型安全。例如:

public class Hole<T> {
   private T objectInHole;

   public void putInHole(T object) {
      this.objectInHole = object;
   }
   public T getOutOfHole() {
      return objectInHole;
   }

   public String toString() {
      return objectInHole.toString();
   }
   .....equals, hashCode, etc.
}


Hole<String> foo = new Hole<String)();
foo.putInHole(new String());
System.out.println(foo); //this prints nothing
thisWorks(foo);
System.out.println(foo);//this prints howdy

public static void thisWorks(Hole<String> foo){
   foo.putInHole("howdy");
}

【讨论】:

  • 好答案,代码示例的最后一部分可能有点做作,分散了您在上面写的重要内容的注意力。
  • “不受控制的”指针可能会造成混淆和风险。并非所有语言都使用像 C/C++ 这样的指针,并让您随机指向奇怪的位置或对象。 Java 只是具有良好控制的指针。
【解决方案4】:

您提出的问题实际上与按值传递、按引用传递或字符串不可变这一事实无关(正如其他人所说)。

在方法内部,您实际上创建了一个局部变量(我将其称为“localFoo”),它指向与原始变量(“originalFoo”)相同的引用。

当您将“howdy”分配给 localFoo 时,您不会更改 originalFoo 指向的位置。

如果你做了类似的事情:

String a = "";
String b = a;
String b = "howdy"?

你会期待吗:

System.out.print(a)

打印出“你好”?它打印出“”。

您无法通过更改 localFoo 指向的内容来更改 originalFoo 指向的内容。您可以修改两者都指向的对象(如果它不是不可变的)。例如,

List foo = new ArrayList();
System.out.println(foo.size());//this prints 0

thisDoesntWork(foo);
System.out.println(foo.size());//this prints 1

public static void thisDoesntWork(List foo){   
    foo.add(new Object);
}

【讨论】:

  • 它与按值传递与按引用传递有关。使用 real 传递引用参数(例如 C# 中的“ref”),您可以通过引用传递字符串变量,它会被更改。
【解决方案5】:

在 java 中,所有传递的变量实际上都是由值甚至对象传递的。传递给方法的所有变量实际上都是原始值的副本。在您的字符串示例的情况下,原始指针(它实际上是一个引用 - 但为避免混淆,请使用不同的词)被复制到一个新变量中,该变量成为该方法的参数。

如果一切都是参考,那就太痛苦了。一个人需要在整个地方制作私人副本,这绝对是一种真正的痛苦。每个人都知道,对值类型等使用不变性可以使您的程序无限简单和更具可扩展性。

一些好处包括: - 无需制作防御性副本。 - 线程安全 - 无需担心锁定,以防万一其他人想要更改对象。

【讨论】:

  • Java 中的一切都是通过引用实现的。
  • 错错错了。原始值不是通过引用。您不能将对存在于一个方法中的“int”的引用传递给另一个方法并修改第一个局部变量。
  • 嗯,每个对象都是通过引用传递的。无论如何,通过引用传递值类型没有意义。
  • @Axelle,不,对象不是通过引用传递的。通过引用传递值类型确实有意义。请使用允许真正的按引用传递语义的语言(例如,使用“ref”参数的 C#)。 Java 严格按值传递。
  • @jon,不是语言,而是阻止 java 支持'by ref'的平台。
【解决方案6】:

问题是您正在实例化 Java 引用类型。然后将该引用类型传递给静态方法,并将其重新分配给本地范围的变量。

它与不变性无关。可变引用类型也会发生完全相同的事情。

【讨论】:

    【解决方案7】:

    如果我们做一个粗略的 C 和汇编类比:

    void Main()
    { 
         // stack memory address of message is 0x8001.  memory address of Hello is 0x0001.  
         string message = "Hello"; 
         // assembly equivalent of: message = "Hello";
         // [0x8001] = 0x0001
    
         // message's stack memory address
         printf("%d", &message); // 0x8001
    
         printf("%d", message); // memory pointed to of message(0x8001): 0x0001
         PassStringByValue(message); // pass the pointer pointed to of message.  0x0001, not 0x8001
         printf("%d", message); // memory pointed to of message(0x8001): 0x0001.  still the same
    
         // message's stack memory address doesn't change
         printf("%d", &message); // 0x8001
    }
    
    void PassStringByValue(string foo)
    {
        printf("%d", &foo); // &foo contains foo's *stack* address (0x4001)
    
        // foo(0x4001) contains the memory pointed to of message, 0x0001
        printf("%d", foo);  // 0x0001
        // World is in memory address 0x0002
        foo = "World";  // on foo's memory address (0x4001), change the memory it pointed to, 0x0002
        // assembly equivalent of: foo = "World":
        // [0x4001] = 0x0002
    
        // print the new memory pointed by foo
        printf("%d", foo); // 0x0002
    
        // Conclusion: Not in any way 0x8001 was involved in this function.  Hence you cannot change the Main's message value.
        // foo = "World"  is same as [0x4001] = 0x0002
    
    }
    

    void Main()
    {
         // stack memory address of message is 0x8001.  memory address of Hello is 0x0001.  
         string message = "Hello"; 
         // assembly equivalent of: message = "Hello";
         // [0x8001] = 0x0001
    
         // message's stack memory address
         printf("%d", &message); // 0x8001
    
         printf("%d", message); // memory pointed to of message(0x8001): 0x0001
         PassStringByRef(ref message); // pass the stack memory address of message.  0x8001, not 0x0001
         printf("%d", message); // memory pointed to of message(0x8001): 0x0002. was changed
    
         // message's stack memory address doesn't change
         printf("%d", &message); // 0x8001
    }
    
    
    void PassStringByRef(ref string foo)
    {
        printf("%d", &foo); // &foo contains foo's *stack* address (0x4001)
    
        // foo(0x4001) contains the address of message(0x8001)
        printf("%d", foo);  // 0x8001
        // World is in memory address 0x0002
        foo = "World"; // on message's memory address (0x8001), change the memory it pointed to, 0x0002
        // assembly equivalent of: foo = "World":
        // [0x8001] = 0x0002;
    
    
        // print the new memory pointed to of message
        printf("%d", foo); // 0x0002
    
        // Conclusion: 0x8001 was involved in this function.  Hence you can change the Main's message value.
        // foo = "World"  is same as [0x8001] = 0x0002
    
    }
    

    Java 中一切都通过值传递的一个可能原因是,它的语言设计人员希望简化语言并以 OOP 方式完成所有事情。

    他们宁愿让你设计一个使用对象的整数交换器,也不愿他们为按引用传递提供一流的支持,委托也是如此(Gosling 对指向函数的指针感到恶心,他宁愿将这个功能塞进对象)和枚举。

    他们过度简化(一切都是对象)语言,从而损害了对大多数语言结构没有一流的支持,例如想到通过引用、委托、枚举、属性传递。

    【讨论】:

      【解决方案8】:

      你确定它打印的是 null 吗?我认为它只是空白,因为当您初始化提供空字符串的 foo 变量时。

      thisDoesntWork 方法中 foo 的赋值不会改变类中定义的 foo 变量的引用,因此 System.out.println(foo) 中的 foo 仍将指向旧的空字符串对象。

      【讨论】:

      • 它没有用空字符串初始化,AFAICS。
      • 醋我的意思是空字符串对象
      【解决方案9】:

      戴夫,你必须原谅我(好吧,我猜你不是“必须”,但我宁愿你这样做),但这种解释并不过分令人信服。安全性收益相当小,因为任何需要更改字符串值的人都会找到一种方法来通过一些丑陋的解决方法来做到这一点。还有速度?!您自己(非常正确)断言带有 + 的整个业务非常昂贵。

      你们其他人,请理解我知道它是如何工作的,我在问为什么它会这样工作......请停止解释这些方法之间的区别。

      (老实说,我并不是在寻找任何形式的战斗,顺便说一句,我只是不明白这是一个理性的决定)。

      【讨论】:

      • 好吧,你显然遗漏了一些东西。您展示的示例与名称传递无关它只是参数如何在方法内部绑定的问题(实际上有助于理解参数和变量之间的区别)。跨度>
      • @Dr.Dredel:为了简单起见,无论是在语言中还是在阅读代码时。当我调用一个方法并传入一个字符串引用时,我知道该方法不会改变它。虽然恶意代码可以(在某些情况下)绕过它,但更常见的情况是非恶意代码 - 您只是希望能够(续)
      • 轻松推理代码。当您按值传递引用时,更容易理解代码的作用。在 C# 中,您可以通过引用传递参数 - 但仍然很少见,因为它使代码更难理解。
      【解决方案10】:

      @Axelle

      你真的知道按值传递和按引用传递的区别吗?

      在 java 中,甚至引用也是按值传递的。当您传递对对象的引用时,您将在第二个变量中获得引用指针的副本。这就是为什么可以更改第二个变量而不影响第一个变量的原因。

      【讨论】:

      • 是的。是的,我愿意。我只是对 Java 实现 String 的(看似)迟钝的方式感到困惑。这是一个对象,我应该能够将它的一个成员(持有字符的那个)修改为我想要的任何东西,无论它去哪里。你传入一个可以 add() 的列表,对吧?
      • 使字符串不可变是 Java 设计人员的一个伟大决定(这就是为什么在 .NET 中也使用该决定的原因)。否则我们会在整个地方进行防御性抄袭...
      【解决方案11】:

      这是因为,它在方法内部创建了一个局部变量。什么是简单的方法(我很确定会起作用)是:

      String foo = new String();    
      
      thisDoesntWork(foo);    
      System.out.println(foo); //this prints nothing
      
      public static void thisDoesntWork(String foo) {    
         this.foo = foo; //this makes the local variable go to the main variable    
         foo = "howdy";    
      }
      

      【讨论】:

      • -1 OP 并未尝试修复代码。这没有回答 OP 的问题:有人可以向我解释这种设计选择背后的原因是什么吗?
      【解决方案12】:

      如果您认为对象只是对象中的字段,那么在 Java 中对象是通过引用传递的,因为方法可以修改参数的字段,而调用者可以观察到修改。但是,如果您还将对象视为它的标识,那么对象将按值传递,因为方法无法以调用者可以观察到的方式更改参数的标识。所以我会说Java是按值传递的。

      【讨论】:

        【解决方案13】:

        这是因为在“thisDoesntWork”内部,您实际上是在破坏 foo 的本地值。如果你想以这种方式通过引用传递,总是可以将String封装在另一个对象中,比如在一个数组中。

        class Test {
        
            public static void main(String[] args) {
                String [] fooArray = new String[1];
                fooArray[0] = new String("foo");
        
                System.out.println("main: " + fooArray[0]);
                thisWorks(fooArray);
                System.out.println("main: " + fooArray[0]);
            }
        
            public static void thisWorks(String [] foo){
                System.out.println("thisWorks: " + foo[0]);
                foo[0] = "howdy";
                System.out.println("thisWorks: " + foo[0]);
            }
        }
        

        结果如下:

        main: foo
        thisWorks: foo
        thisWorks: howdy
        main: howdy
        

        【讨论】:

          【解决方案14】:

          引用类型的参数作为对对象本身的引用传递(而不是对引用对象的其他变量的引用)。您可以在已传递的对象上调用方法。但是,在您的代码示例中:

          public static void thisDoesntWork(String foo){
              foo = "howdy";
          }
          

          您只是将字符串"howdy" 的引用存储在该方法的本地变量中。调用该方法时,该局部变量 (foo) 已初始化为调用者的 foo 的值,但没有引用调用者的变量本身。初始化后:

          caller     data     method
          ------    ------    ------
          (foo) -->   ""   <-- (foo)
          

          在你的方法赋值之后:

          caller     data     method
          ------    ------    ------
          (foo) -->   ""
                    "hello" <-- (foo)
          

          您还有另一个问题:String 实例是不可变的(出于安全考虑,设计为不可变),因此您无法修改其值。

          如果您真的希望您的方法为您的字符串提供初始值(或在其生命中的任何时间),那么让您的方法返回 String 值,您在调用时分配给调用者的变量。像这样的东西,例如:

          String foo = thisWorks();
          System.out.println(foo);//this prints the value assigned to foo in initialization 
          
          public static String thisWorks(){
              return "howdy";
          }
          

          【讨论】:

          • 不,引用类型不是通过引用传递的。参数(它是一个引用)的值是按值传递的。这与传递引用语义相同。
          • Java 使用复制语义,它会将引用的副本传递给方法。因此,引用是按值传递的。
          【解决方案15】:

          去 suns 网站上做真正的大教程。

          您似乎不了解变量范围的差异。 “foo”对于您的方法来说是本地的。该方法之外的任何东西都不能改变“foo”点。引用您的方法的“foo”是一个完全不同的字段-它是封闭类上的静态字段。

          范围特别重要,因为您不希望系统中的所有其他内容都可以看到所有内容。

          【讨论】:

          • 不,我非常了解范围界定。我不理解无法更改传递给方法的非原始对象的值的价值。我想,最终,我只是渴望选择做任何一个,就像 C 中的情况一样,并询问为什么在 Java 中删除了它。
          • 再看看那乱七八糟的C,没有任何形式的封装。如果幸运的话,系统中任何地方的任何代码行都可以神奇地改变值并破坏您的程序。
          • 德雷德尔博士。你的问题措辞不好。从您的示例中可以看出,您也不了解范围。如果你这样做了,你会感激引用也是范围的。在传递对象引用时,您将获得引用指针的副本。对象可以有很多指向它们的指针
          • 我不必认真为 C 辩护,对吧?
          • 通过引用传递等同于传递指针——有一些限制。按价值简化了这个问题并导致更好的封装。开始思考 C 存在的问题,然后思考为什么 SUN 在设计 Java 时只是简单的某些方面。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-10-30
          • 2015-03-24
          • 2023-03-03
          • 1970-01-01
          • 1970-01-01
          • 2011-11-05
          • 2011-02-04
          相关资源
          最近更新 更多