簡體   English   中英

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

[英]Can someone explain to me what the reasoning behind passing by “value” and not by “reference” in Java is?

我是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

為了得到最后的語句打印“你好”,B必須指向內存中的同一個“洞”, 一個點(指針)。 當你想通過引用傳遞時,這就是你想要的。 Java決定不去這個方向有幾個原因:

  • 指針令人困惑 Java的設計者試圖刪除一些關於其他語言的更令人困惑的事情。 指針是C / C ++最容易被誤解和使用不當的結構之一,還有運算符重載。

  • 指針是安全風險指針在誤用時會導致許多安全問題。 惡意程序會將內容分配給內存的那一部分,然后您認為您的對象實際上是其他人的內容。 (Java已經擺脫了最大的安全問題,緩沖區溢出,帶有已檢查的數組)

  • 抽象泄漏當你開始處理“內存中的內容和位置”時,你的抽象變得不那么抽象了。 雖然抽象泄漏幾乎肯定會侵入一種語言,但設計師並不想直接烘焙它。

  • 對象都是你關心的在Java中,一切都是對象,而不是對象占據的空間。 添加指針會使空間占據重要的對象,盡管.......

您可以通過創建“Hole”對象來模擬您想要的內容。 您甚至可以使用泛型來使其類型安全。 例如:

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

您提出的問題與傳遞值,傳遞引用或字符串不可變這一事實並沒有關系 (正如其他人所說)。

在方法內部,您實際創建了一個局部變量(我稱之為“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方式完成所有操作。

他們寧願讓你設計一個使用對象的整數交換器,而不是它們為by-reference傳遞提供第一類支持,對於委托也是如此(Gosling對指向函數的指針感覺很狡猾,他寧願將這些功能塞進對象)和枚舉。

它們過度簡化(一切都是對象)語言而不利於沒有對大多數語言結構的第一類支持,例如通過引用,委托,枚舉,屬性傳遞。

你確定它打印為空嗎? 我認為它只是空白,因為當你初始化你提供空字符串的foo變量時。

在thisDoesntWork方法中分配foo不會改變類中定義的foo變量的引用,因此System.out.println(foo)中的foo仍將指向舊的空字符串對象。

戴夫,你必須原諒我(好吧,我猜你不是“必須”,但我寧願你這樣做)但這個解釋並不過分令人信服。 安全性收益相當小,因為任何需要更改字符串值的人都會找到一種方法來處理一些丑陋的解決方法。 和速度?! 你自己(非常正確地)聲稱與+的整個業務非常昂貴。

其他人,請理解我知道它是如何工作的,我問為什么它的工作原理...請停止解釋方法之間的區別。

(老實說,我不是在尋找任何形式的戰斗,順便說一句,我只是看不出這是一個理性的決定)。

@Axelle

Mate你真的知道傳遞價值和參考之間的區別嗎?

在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”是一個完全不同的字段 - 它是封閉類的靜態字段。

范圍界定尤為重要,因為您不希望系統中的其他所有內容都可見。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM