有人可以向我解释在Java中传递“值”而不是“引用”背后的原因是什么?

我是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不同,用户可以对传递给方法的东西进行操作(并且不会被overloaded =设置的值所困)。

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

这个咆哮解释得比我甚至尝试过:

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

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

在考虑字符串在Java中的行为方式时要记住的另一件事是字符串是不可变的。 它在C#中的工作方式相同,原因如下:

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

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

由于我的原始答案是“为什么会发生”,而不是“为什么语言设计得如此发生”,我会再给它一个。

为了简化操作,我将摆脱方法调用并以另一种方式显示正在发生的事情。

 String a = "hello"; String b = a; String b = "howdy" System.out.print(a) //prints hello 

要获得打印“hello”的最后一个语句, b必须指向内存中指向(指针)的相同“洞”。 当你想通过引用传递时,这就是你想要的。 Java决定不去这个方向有几个原因:

  • 指针令人困惑 Java的设计者试图删除一些关于其他语言的更令人困惑的事情。 指针是C / C ++最容易被误解和使用不当的结构之一,还有运算符重载。

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

  • 抽象泄漏当你开始处理“内存中的内容和位置”时,你的抽象就变得不那么抽象了。 虽然抽象泄漏几乎肯定会侵入一种语言,但设计师并不想直接烘焙它。

  • 对象都是你关心的在Java中,一切都是对象,而不是对象所占据的空间。 添加指针会使空间占据重要的对象,尽管…….

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

 public class Hole { 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 foo = new Hole foo){ foo.putInHole("howdy"); } 

您提出的问题与传递值,传递引用或字符串不可变这一事实并没有关系 (正如其他人所说)。

在方法内部,您实际创建了一个局部变量(我称之为“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); } 

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

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

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

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

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

如果我们将粗略的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对指向函数的指针感觉很蠢,他宁愿将这些function塞进对象中)和枚举。

它们过度简化(一切都是对象)语言而不利于没有对大多数语言结构的第一类支持,例如通过引用,委托,枚举,属性传递。

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

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

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

其他人,请理解我知道它是如何工作的,我问为什么它的工作原理…请停止解释方法之间的区别。

(老实说,我不是在寻找任何forms的战斗,顺便说一下,我只是看不出这是一个理性的决定)。

@Axelle

伙计你真的知道传递价值和参考之间的区别吗?

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

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

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

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

这是因为在“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 

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

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

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

您似乎无法理解变量的差异范围。 “foo”是您的方法的本地。 除了那个方法之外什么都不能改变“foo”指向的东西。 引用你的方法的“foo”是一个完全不同的字段 – 它是封闭类的静态字段。

范围界定尤其重要,因为您不希望系统中的其他所有内容都可见。