1. 程式人生 > >關於JAVA中String類以形參傳遞到函式裡面,修改後外面引用不能獲取到更改後的值

關於JAVA中String類以形參傳遞到函式裡面,修改後外面引用不能獲取到更改後的值

一、 最開始的示例
寫程式碼最重要的就是實踐,不經過反覆試驗而得出的說辭只能說是憑空遐想罷了。所以,在本文中首先以一個簡單示例來丟擲核心話題:

public class StringAsParamOfMethodDemo {

public static void main(String[] args) {
StringAsParamOfMethodDemo StringAsParamOfMethodDemo = 
new StringAsParamOfMethodDemo();
StringAsParamOfMethodDemo.testA();
}

private void testA
() { String originalStr = "original"; System.out.println("Test A Begin:"); System.out.println("The outer String: " + originalStr); simpleChangeString(originalStr); System.out.println("The outer String after inner change: " + originalStr); System.out.println("Test A End."); System.out.println(); } public
void simpleChangeString(String original) { original = original + " is changed!"; System.out.println("The changed inner String: " + original); } }

這段程式碼的邏輯是這樣的:先賦值一個String型別的區域性變數,然後把這個變數作為引數送進一個方法中,在這個方法中改變該變數的值。編譯執行之後,發現輸出結果是這樣的:

Test A Begin:
The outer String: original
The changed inner String
: original is changed! The outer String after inner change: original Test A End.

這個結果表明在方法內部對String型別的變數的重新賦值操作並沒有對這個String變數的原型產生任何影響。好了,這個示例的邏輯和執行結果都展示清楚了,接下來我們來對這個小程式進行分析。在這之前我們先來回顧下Java中所謂的“傳值”和“傳引用”問題。

二、 Java中的“傳值”和“傳引用”問題
許多初學Java的程式設計師都在這個問題上有所思索,那是因為這是所謂的“C語言的傳值和傳指標問題”在Java語言上同類表現。
最後得出的結論是:
在Java中,當基本型別作為引數傳入方法時,無論該引數在方法內怎樣被改變,外部的變數原型總是不變的,因為方法內部有外部變數的一份拷貝,對這個拷貝的更改不會改變外部變數的值。程式碼類似上面的示例:

int number = 0;
changeNumber(number) {number++}; //改變送進的int變數
System.out.println(number); //這時number依然為0

這就叫做“值傳遞”,即方法操作的是引數變數(也就是原型變數的一個值的拷貝)改變的也只是原型變數的一個拷貝而已,而非變數本身。所以變數原型並不會隨之改變。

但當方法傳入的引數為非基本型別時(也就是說是一個物件型別的變數), 方法裡面改變引數變數的同時變數原型也會隨之改變,程式碼同樣類似上面的示例:

StringBuffer strBuf = new StringBuffer(“original”);
changeStringBuffer(strBuf) {strbuf.apend(“ is changed!”)} //改變送進的StringBuffer變數
System.out.println(strBuf); //這時strBuf的值就變為了original is changed! 

這 種特性就叫做“引用傳遞”,也叫做傳址,即方法操作引數變數時是拷貝了變數的引用,注意下傳遞給方法的引數為變數的引用,其實也就是指標,而後通過這個引用找到變數(在這裡是物件)的真正地址,並對其進行操作。當 該方法結束後,方法內部的那個引數變數隨之消失。但是要知道這個變數只是物件的一個引用而已,它只是指向了物件所在的真實地址,而非物件本身,所以它的消 失並不會帶來什麼負面影響。回頭來看原型變數,原型變數本質上也是那個物件的一個引用(和引數變數是一樣一樣的),當初對引數變數所指物件的改變就根本就 是對原型變數所指物件的改變。所以原型變數所代表的物件就這樣被改變了,而且這種改變被儲存了下來。

瞭解了這個經典問題,很多細心的讀者肯定會立刻提出新的疑問:“可是String型別在Java語言中屬於非基本型別啊!它在方法中的改變為什麼沒有被保 存下來呢!”的確,這是個問題,而且這個新疑問幾乎推翻了那個經典問題的全部結論。真是這樣麼?好,現在我們就來繼續分析。

三、 關於String引數傳遞問題的曲解之一——直接賦值與物件賦值
String型別的變數作為引數時怎麼會像基本型別變數那樣以傳值方式傳遞呢?關於這個問題,有些朋友給出過解釋。
一 種解釋就是,對String型別的變數賦值時並沒有new出物件,而是直接用字串賦值,所以Java就把這個String型別的變數當作基本型別看待 了。即,應該String str = new String(“original”);,而不是String str = “original”;。這種因為給String型別變數賦值方式不同是造成問題所在麼?我們來為先前的示例稍微改造下,執行之後看看結果就知道了。改造後的程式碼如下:

private void testB() {
String originalStr = new String("original");
System.out.println("Test B Begin:");
System.out.println("The outer String: " + originalStr);
changeNewString(originalStr);
System.out.println("The outer String after inner change: " + originalStr);
System.out.println("Test B End:");
System.out.println();
}

public void changeNewString(String original) {
original = new String(original + " is changed!");
System.out.println("The changed inner String: " + original);
}

我們來看看這次執行結果是怎麼樣的:

Test B Begin:
The outer String: original
The changed inner String: original is changed!
The outer String after inner change: original
Test B End.

實踐證明,這種說法是錯的。
實際上,字串直接賦值和用new出的物件賦值的區別僅僅在於儲存方式不同。
簡單說明下:
字 符串直接賦值時,String型別的變數所引用的值是儲存在類的常量池中的。因為”original”本身是個字串常量,另一方面String是個不可 變型別,所以這個String型別的變數相當於是滴對一個常量的引用。這種情況下,變數的記憶體空間大小是在編譯期就已經確定的。
new物件的方式是將”original”儲存到String物件的記憶體堆空間中,而這個儲存動作是在執行期進行的。在這種情況下,Java並不是把”original”這個字串當作常量對待的,因為這時它是作為建立String物件的引數出現的。
所以對String的賦值方式和其引數傳值問題並沒有直接聯絡。總之,這種解釋並不是正解。
四、 關於String引數傳遞問題的曲解之二——“=”變值與方法賦值的區別上面
又有些朋友認為,變值不同步的問題是處在改變值的方式上。
這種說法認為:“在Java 中,改變引數的值有兩種情況,第一種,使用賦值號“=”直接進行賦值使其改變;第二種,對於某些物件的引用,通過一定途徑對其成員資料進行改變,如通過物件本身的成員方法。認為對於第一種情況,其改變不會影響到被傳入該引數變數的方法以外的資料,或者直接說不會改變原來的資料。而第二種方法,則相反,會影響到源資料——因為引用指向的物件沒有變,對其成員資料進行改變那麼實質上改變的是該物件。”這種觀點說必須用類的成員變數改變成員資料才可以成功改變成員資料。
這種方式聽起來似乎有些…,我們還是用老辦法,編寫demo,做個小試驗,程式碼如下:

private void testC() {
String originalStr = new String("original");
System.out.println("Test C Begin:");
System.out.println("The outer String: " + originalStr);
changeStrWithMethod(originalStr);
System.out.println("The outer String after inner change: " + originalStr);
System.out.println("Test C End.");
System.out.println();
}

private static void changeStrWithMethod(String original) {
original = original.concat(" is changed!");
System.out.println("The changed inner String: " + original);
}

結果如下:

Test C Begin:
The outer String: original
The changed inner String: original is changed!
The outer String after inner change: original
Test C End.

怎麼樣,這證明了問題並不是出在這,
那到底是什麼原因導致了這種狀況呢?
好了,下面說下我的解釋。

真正答案

這個問題真正原因是因為String類的儲存是通過final修飾的char[]陣列來存放結果的。不可更改。所以每次當外部一個String型別的引用傳遞到方法內部時候,只是把外部String型別變數的引用傳遞給了方法引數變數。對的。外部String變數和方法引數變數都是實際char[]陣列的引用而已。所以當我們在方法內部改變這個引數的引用時候,因為char[]陣列不可改變,所以每次新建變數都是新建一個新的String例項。很顯然外部String型別變數沒有指向新的String例項。所以也就不會獲取到新的更改。
下面程式例程假定tString指向A記憶體空間,A記憶體空間存放了”hello”這個字串,然後呼叫modst函式將tString引用賦值給了text引用,注意是引用。確實是傳址,我們知道String是不可變的,任何進行更改的操作都會產生新的String例項。所以在方法裡面text指向了B空間,B空間存放了”sdf” 字串,但是這個時候tString還是指向A空間,並沒有指向B空間。

String tString = "hello";
        System.out.println(tString);
        modst(tString);
        System.out.println(tString);
        //改變text指向
public static String modst(String text) {
        return text = "sdf";
    }

兩次輸出結果都是

hello
hello


參考文獻