8

我对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 不同,用户将能够对传递给方法的内容进行操作(并且不会被重载 = 设置的值所困扰)。

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

4

15 回答 15

11

这个咆哮比我什至试图解释得更好:

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

于 2009-02-05T03:59:21.907 回答
9

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

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

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

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

于 2009-02-05T03:55:44.737 回答
5

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

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

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");
}
于 2009-02-05T14:38:39.273 回答
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);
}
于 2009-02-05T04:13:05.263 回答
3

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

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

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

于 2009-02-05T04:07:09.940 回答
2

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

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

于 2009-02-05T04:06:40.923 回答
1

如果我们做一个粗略的 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 对指向函数的指针感到恶心,他宁愿将该功能塞进对象)和枚举。

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

于 2009-02-05T06:05:16.363 回答
0

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

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

于 2009-02-05T03:59:34.137 回答
0

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

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

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

于 2009-02-05T04:13:12.440 回答
0

@Axelle

伙计,您真的知道按值传递和按引用传递之间的区别吗?

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

于 2009-02-05T05:49:26.113 回答
0

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

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";    
}
于 2009-02-05T09:11:05.443 回答
0

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

于 2009-02-05T09:36:03.800 回答
0

这是因为在“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
于 2009-02-05T16:41:00.087 回答
-1

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

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";
}
于 2009-02-05T04:06:11.017 回答
-2

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

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

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

于 2009-02-05T03:56:55.077 回答