一、傳值和傳引用

C++里面有傳值和傳引用的說法,而java里面卻不一樣,公司讓我出一道考查相關的東西,于是出了下面這道題:

class Number {
 int i;
}

public class Assignment {
 public static void main (String [] args) {
  Number n1 = new Number();
  Number n2 = new Number();
  n1.i = 9;
  n2.i = 47;
  System.out.println("1: n1.i: " + n1.i + ", n2.i:" + n2.i);//1: n1.i: 9, n2.i:47
  
  n1 = n2;
  System.out.println("2: n1.i: " + n1.i + ", n2.i:" + n2.i);//2: n1.i: 47, n2.i:47
  
  n1.i = 27;
  System.out.println("3: n1.i: " + n1.i + ", n2.i:" + n2.i);//3: n1.i: 27, n2.i:27
  
  test(n1, n2);
  System.out.println("4: n1.i: " + n1.i + ", n2.i:" + n2.i);//4: n1.i: 6, n2.i:6
  
  StringBuffer sb1 = new StringBuffer ("A");
  StringBuffer sb2 = new StringBuffer ("B");
  
  test(sb1,sb2);
  System.out.println(sb1 + "." + sb2);//AB.B
  
  sb2 = sb1;
  System.out.println(sb1 + "." + sb2);//AB.AB
  
  String s1 = new String("A");
  String s2 = new String("B");
  
  test(s1, s2);
  System.out.println("String: "+ s1 + "." + s2);//String: A.B
  
 }
 static void test (Number n1, Number n2)
 {
  n1.i = 6;
  n2 = n1;
 }
 
 static void test (String s1, String s2)
 {
  s1 = s2;  //賦值時,s1就重新指向了s2指向的內容,但是實參的內容沒有改變
  s1 = "C";
 }
 static void test (StringBuffer sb1, StringBuffer sb2)
 {
  sb1.append ("B");//沒有產生新對象
  sb2 = sb1;
 }
}

在Java中,事實上底層工作原理不存在傳引用的概念,這也象《Practical Java》中所說的那樣,Java中只有傳值。這句話理解起來需要費一定的周折。
傳值和傳引用的問題一直是Java里爭論的話題。與C++不同的,Java里面沒有指針的概念,Java的設計者巧妙的對指針的操作進行了管理。


下面舉個簡單的例子,說明什么是傳值,什么是傳引用。
//例1
void method1(){
int x=0;
this.change(x);
System.out.println(x);
}

void int change(int i){
 i=7;
}

很顯然的,在mothod1中執行了change(x)后,x的值并不會因為change方法中將輸入參數賦值為1而變成1,也就是說在執行 change(x)后,x的值z依然是0。這是因為x傳遞給change(int i)的是值。這就是最簡單的傳值。


同樣的,進行一點簡單的變化。
//例2
void method1(){
StringBuffer x=new StringBuffer("Hello");
this.change(x);
System.out.println(x);
}

void int change(StringBuffer i){
 i.append(" world!");
}
看起來沒什么變化,但是這次mothed1中執行了change (x)后,x的值不再是"Hello"了,而是變成了"Hello world!"。這 是因為x傳遞給change(i)的是x的引用。這是最經典的傳引用。
似乎有些奇怪了,兩段程序沒有特別的不同,可是為什么一個傳的是值而另一個傳的是引用呢?

Java 提出的思想,在Java里面任何東西都是類。但是Java里面同時還有簡單數據類型:int,byte,char,boolean,與這些數據類型相對應的類是Integer,Byte,Character,Boolean,這樣做依然不會破壞Java關于任何東西都是類的提法。這里提到數據類型和類似乎和我們要說的傳值和傳引用的問題無關,但這是我們分辨傳值和傳引用的基礎。

我們分析一下上面的幾個例子:
先看例1,即使你不明白為什么,但是你應該知道這樣做肯定不會改變x的值。為了方便說明,我們給例子都加上行號。
//例1
1 void method1(){
2  int x=0;
3  this.change(x);
4 }
5
6 void int change(int i){
7 i=7;
8}
讓我們從內存的存儲方式看一下x和I之間到底是什么關系。
在執行到第2行的時候,變量x指向一個存放著int 0的內存地址。

變量x---->[存放值0]

執行第3行調用change(x)方法的時候,內存中是這樣的情形:x把自己值在內存中復制一份,然后變量i指向這個被復制出來的0。

變量x---->[存放值0]
              ↓進行了一次值復制
變量i---->[存放值0]

這時候再執行到第7行的時候,變量i的被賦值為7,而這一步的操作已經跟x沒有任何關系了。

變量x---->[存放值0]
             
變量i---->[存放值7]

說到這里應該已經理解為什么change(x)不能改變x的值了吧?因為這個例子是傳值的。


那么,試著分析一下為什么例三中的switchValue()方法不能完成變量值交換的工作?
再看例2。
//例2
1void method1(){
2 StringBuffer x=new StringBuffer("Hello");
3 this.change(x);
4}
5
6 void change(StringBuffer i){
7 i.append(" world!");
8}
例2似乎和例1從代碼上看不出什么差別,但是執行結果卻是change(x)能改變x的值。依然才從內存的存儲角度來看看例2的蹊蹺在哪里。
在執行到第2行時候,同例1一樣,x指向一個存放"Hello"的內存空間。

變量x---->[存放值"Hello"]

接下來執行第三行change(x),注意,這里就與例1有了本質的不同:調用change(x)時,變量i也指向了x指向的內存空間,而不是指向x的一個拷貝。

變量x "
       -->[存放值"Hello"]
變量i /

于是,第7行對i調用append方法,改變i指向的內存空間的值,x的值也就隨之改變了。

變量x "
       -->[追加為"Hello World!"]
變量i /

為什么x值能改變呢?因為這個例子是傳引用的。

對于參數傳遞,如果是簡單數據類型,那么它傳遞的是值拷貝,對于類的實例它傳遞的是類的引用

需要注意的是,這條規則只適用于參數傳遞。為什么這 么說呢?我們看看這樣一個例子:
//例5
String str="abcdefghijk";
str.replaceAll("b","B");
這兩句執行后,str的內容依然是"abcdefghijk",但是我們明明是對str操作的,為什么是這樣的呢?因為str的值究竟會不會被改變完全取 決于replaceAll這個方法是怎么實現的。類似的,有這樣一個例子:
//例6
1 void method1() {
2 StringBuffer x = new StringBuffer("Hello");
3 change1(x);
4 System.out.println(x);
5 }
6
7 void method2() {
8 StringBuffer x = new StringBuffer("Hello");
9 change2(x);
10 System.out.println(x);
11 }
12
13 void change1(StringBuffer sb) {
14 sb.append(" world!");
15 }
16
17 void change2(StringBuffer sb) {
18 sb = new StringBuffer("hi");
19 sb.append(" world!");
20 }
調用method1(),屏幕打印結果為:"Hello world!"
調用method2(),我們認為結果應該是"hi world",因為sb傳進來的是引用。可是實際執行的結果是"Hello"!
難道change2()又變成傳值了?!其實change1()和change2()的確都是通過參數傳入引用,但是在方法內部因為處理方法的不同而使結果大相徑庭。


所以,還有一條不成規則的規則:對于函數調用,最終效果是什么完全看函數內部的實現。比較標準的做法是如果會改變引用的內容,則使用void作為方法返回值,而不會改變引用內容的則在返回值中返回新的值。

二、相等判斷

在使用vector中的contains方法時,重載了vector中對象的類DataDictionry的equals
方法,方法如下:

    public boolean equals(Object obj) {
        
if(this.code == ((DataDictionry)obj).code)
            
return true;
        
else
            
return false;

    }

    相等的條件根據業務,code字符串內容屬性相等即可。即如果有一個DataDictionry對象和另一個DataDictionry的code相等即兩個對象為同一個對象。但是發現運行結果有問題,即使兩個的code的內容相等
程序也判斷是false。但是改成如下:
  
public boolean equals(Object obj) {
        
if(this.code.equals(((DataDictionry)obj).code))
            
return true;
        
else
            
return false;

    }
   則結果是true。

   因為對于字符串來說用這兩種方式判斷的結果是一樣的?什么情況下兩個字符串用equals判斷是相等的,但是用“==”判斷則不相等呢?

   其實并不是字符串的比較“==”和equals方法的結果是一樣的,而是根據字符串的初始化相關。

 1 public class TestString {
 2 
 3     public static void main(String[] args) {
 4         String a = "0010";
 5         String b = "0010";
 6        
 7         if(a == b)
 8             System.out.println("equals");
 9         else
10             System.out.println("not equals");
11         /* print equals */
12        
13         if(a.equals(b))
14             System.out.println("equals");
15         else
16             System.out.println("not equals");
17         /* print equals */
18        
19         String s1 = new String("abc");
20         String s2 = new String("abc");
21        
22         if(s1 == s2)
23             System.out.println("equals");
24         else
25             System.out.println("not equals");
26        
27         /* print not equals */
28        
29         if(s1.equals(s2))
30             System.out.println("equals");
31         else
32             System.out.println("not equals");
33        
34         /* print not equals */
35    
36     }
37 
38 }


  所以如果不能肯定是new產生的還是直接賦值得到的字符串進行比較,都使用equals方法是沒有問題的。
本質上說,“ ==”還是比較的引用地址,equals比較具體的內容(String等那些重載了equals方法的類)。