Java中String型別的引數傳遞問題的解析
一、引入示例
Java程式碼- <span style="font-size: small;">public class StringAsParamOfMethodDemo {
- public static void main(String[] args) {
- StringAsParamOfMethodDemo sapm = new StringAsParamOfMethodDemo();
- sapm.testA();
- }
- private void testA() {
-
String originalStr = "original"
- System.out.println("=======Test A Begin:=======");
- System.out.println("1.The outer String:\n" + originalStr);
- simpleChangeString(originalStr);
- System.out.println("3.The outer String after inner change:\n" + originalStr);
-
System.out.println("=======Test A End.======="
- System.out.println();
- }
- public void simpleChangeString(String original){
- original = original + " is changed!";
- System.out.println("2.The changed inner String:\n" + original);
- }
- </span>
這段程式碼的邏輯是這樣的:先賦值一個String型別的區域性變數,然後把這個變數作為引數送進一個方法中,在這個方法中改變該變數的值。
編譯執行之後,發現輸出結果是這樣的:
=======Test A Begin:=======
1.The outer String:
original
2.The changed inner String:
original is changed!
3.The outer String after inner change:
original
=======Test A End.=======
結果表明在方法內部對String型別的變數的重新賦值操作並沒有對這個變數的原型產生任何影響。
好了,這個示例的邏輯和執行結果都展示清楚了,接下來我們來對這個小程式進行分析。在這之前我們先來回顧下Java中所謂的“傳值”和“傳引用”問題。
二、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”;。這是問題所在麼?我們來為先前的示例稍微改造下,執行之後看看結果就知道了。改造後的程式碼如下
Java程式碼- <span style="font-size: small;"> private void testB() {
- String originalStr = new String("original");
- System.out.println("======= Test B Begin =======");
- System.out.println("1.The outer String: " + originalStr);
- changeNewString(originalStr);
- System.out.println("3.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("2.The changed inner String: " + original);
- }</span>
執行結果是:
======= Test B Begin =======
1.The outer String: original
2.The changed inner String: original is changed!
3.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,做個小試驗,程式碼如下:
Java程式碼- private void testB() {
- String originalStr = new String("original");
- System.out.println("======= Test B Begin =======");
- System.out.println("1.The outer String: " + originalStr);
- changeNewString(originalStr);
- System.out.println("3.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("2.The changed inner String: " + original);
- }
結果如下:
=========Test C Begin=========
1.The outer String: original
2.The changed inner String: original is changed!
3.The outer String after inner change: original
=========Test C End=========
這證明了問題並不是出在這,又一個解釋在實踐論據下夭折了。
那到底是什麼原因導致了這種狀況呢?
五、String引數傳遞問題的癥結所在
Java程式碼- public String(String original) {
- int size = original.count;
- char[] originalValue = original.value;
- char[] v;
- if (originalValue.length > size) {
- // The array representing the String is bigger than the new
- // String itself. Perhaps this constructor is being called
- // in order to trim the baggage, so make a copy of the array.
- int off = original.offset;
- v = Arrays.copyOfRange(originalValue, off, off+size);
- } else {
- // The array representing the String is the same
- // size as the String, so no point in making a copy.
- v = originalValue;
- }
- this.offset = 0;
- this.count = size;
- this.value = v;
- }
也許你注意到了裡面的char[],這說明對String的儲存實際上通過char[]來實現的。怎麼樣?其實就是一層窗戶紙。不知道大家還記不記得在Java API中定義的那些基本型別的包裝類。比如Integer是int包裝類、Float是float的包裝類等等。對這些包裝類的值操作實際上都是通過對其對應的基本型別操作而實現的。是不是有所感悟了?對,String就相當於是char[]的包裝類。包裝類的特質之一就是在對其值進行操作時會體現出其對應的基本型別的性質。在引數傳遞時,包裝類就是如此體現的。所以,對於String在這種情況下的展現結果的解釋就自然而然得出了。同樣的,Integer、Float等這些包裝類和String在這種情況下的表現是相同的。
Integer例子如下:
Java程式碼- private void testD() {
- Integer originalInt = new Integer(123);
- System.out.println("=========Test D Begin=========");
- System.out.println("1.The outer Integer: " + originalInt);
- changeIntWithMethod(originalInt);
- System.out.println("3.The outer Integer after inner change: " + originalInt);
- System.out.println("=========Test D End=========");
- System.out.println();
- }
- private static void changeIntWithMethod(Integer original) {
- original += 456;
- System.out.println("2.The changed inner Integer: " + original);
- }
結果為:
=========Test D Begin=========
1.The outer Integer: 123
2.The changed inner Integer: 579
3.The outer Integer after inner change: 123
=========Test D End=========