1. 程式人生 > >Java String引起的常量池、String類型傳參、“==”、“equals”、“hashCode”問題 細節分析

Java String引起的常量池、String類型傳參、“==”、“equals”、“hashCode”問題 細節分析

怎麽辦 理解 amp 標準 col 要求 oid font 說明

  在學習javase的過程中,總是會遇到關於String的各種細節問題,而這些問題往往會出現在Java攻城獅面試中,今天想寫一篇隨筆,簡單記錄下我的一些想法。話不多說,直接進入正題。

1.String常量池、“==”、“equals”:

先看一段代碼:

 1 String s1 = "123";
 2 String s2 = "123";
 3 System.out.println("s1==s2? "+(s1==s2));//true
 4         
 5 //使用new關鍵字創建一個String對象s3,看看會不會出現不一樣的情況?
 6 String s3 = new String("123");
7 System.out.println("s1==s3? "+(s1==s3));//false 8 9 //如果不使用==比較,而是equals比較呢? 10 System.out.println("s1.equals(s3)? "+s1.equals(s3));//true

運行結果:

1 s1==s2? true
2 s1==s3? false
3 s1.equals(s3)? true

看到這裏,有的人會迷惑了:為什麽s1==s2?為什麽s1==s3是false?而s1.equals(s3)卻是true?

在Java語言中,==和equals都有比較的作用。這兩種方式有什麽區別呢?為什麽要設計出來這兩種方式呢?

我們知道java中有8種基本類型和非基本類型(對象類型或者引用類型)

基本類型有:byte,short,int,long,float,double,boolean,char;

對象類型:除了以上8種基本類型

對於基本類型,使用==就可以直接進行比較是否相等,而對於對象類型,使用==只會比較該對象變量的內存地址,在Java中每個新建的對象都有自己的一塊內存,只要使用了new就是兩個不同的對象,所以此時==顯然不能滿足我們的需求,自然s1==s3會是false。可是我們確實想比較兩個對象變量指向的值,怎麽辦呢?於是,equals()被設計出來了。equals()是Object類中的一個方法,通過查閱Object中equals()方法的API

1 public boolean equals(Object obj) {
2     return (this == obj);
3 }

我們發現:在Object類中equals()方法竟然也是使用了==符號來進行對象的比較!!! 那豈不是完犢子?跟我們想要的功能不一樣啊。可是,我們也應該知道一句話:Java中萬物皆對象,支持面向對象是Java的一大特性,而Object類的存在保證了萬物皆對象,因為Object是所有對象的父類!任何對象被創建後都默認繼承了Object類(根類),擁有了Object類的方法和字段,這就是Java語言的另一個特性:繼承。於是被創建的對象就可以在自己對應的類中,對Object類中的方法進行重寫,例如本例中String類中對equals()方法重寫的代碼是:

 1 public boolean equals(Object anObject) {
 2     if (this == anObject) {
 3         return true;
 4     }
 5     if (anObject instanceof String) {
 6         String anotherString = (String)anObject;
 7         int n = value.length;
 8         if (n == anotherString.value.length) {
 9             char v1[] = value;
10             char v2[] = anotherString.value;
11             int i = 0;
12             while (n-- != 0) {
13                 if (v1[i] != v2[i])
14                     return false;
15                     i++;
16                 }
17                 return true;
18             }
19     }
20     return false;
21 }                

上述的代碼大致表示的是:將兩個字符串拆分成一個字符一個字符地對比,只有兩個字符串的全部字符相等,才返回true,因此實現了比較兩個String對象(對象類型)指向的值是否相等的功能。因此,此時我們明白了為什麽 s1.equals(s3)為true。

那麽現在的問題來了,String類型不是對象類型嗎?對象類型不是不能使用==來進行比較嗎?那為什麽s1==s2會是true?

String常量池就出現在我們的討論中了

為了減少在JVM中創建的字符串的數量,字符串類維護了一個字符串池,每當代碼創建字符串常量時,JVM會首先檢查字符串常量池。如果字符串已經存在池中,就返回池中的實例引用。如果字符串不在池中,就會實例化一個字符串並放到池中。也就是說,在我們使用

String s1 = "123";

這個方式創建s1字符串後,在String常量池中就存在一個實例"123",當第二次創建字符串常量s2時,

String s2 = "123";

由於s2對應的也是“123”,而String常量池中此時已經有“123”,所以就直接將s2指向"123",在此過程中沒有對象的新建。因此,實際上s1和s2是一個對象,所以自然s1==s2 為true;
下面有個思考題給讀者好好思考:(也是經常被面試到的問題)

1 String s1 = new String("你好") ;
2 String s2 = new String("你好") ;

上述代碼中,一共創建幾個String對象?答案:3個。好好思考。(編譯期Constant Pool(常量池)中創建1個,運行期heap(堆)中創建2個)
更多關於常量池的內容,請參考:

https://blog.csdn.net/xdugucc/article/details/78193805

2.“equals”、“hashCode”:

先看一段代碼:

 1 String s1 = "123";
 2 String s2 = new String("123");
 3 System.out.println("s1.equals(s2)? "+s1.equals(s2));//true
 4 
 5 //輸出s1和s2的hashCode
 6 System.out.println("s1,s2的hashCode分別為:");
 7 System.out.println("s1:"+s1.hashCode());//48690
 8 System.out.println("s2:"+s2.hashCode());//48690
 9 
10 //創建一個HashSet
11 Set<String> hashSet = new HashSet<String>();
12 hashSet.add(s1);//將s1加入集合hashSet
13 hashSet.add(s2);//將s2加入集合hashSet
14 
15 //遍歷集合hashSet
16 System.out.println("存儲在hashSet中的元素為:");
17 Iterator<String> it = hashSet.iterator();
18 while(it.hasNext()) {
19     System.out.println(it.next());
20 }

運行結果:

1 s1.equals(s2)? true
2 s1,s2的hashCode分別為:
3 s1:48690
4 s2:48690
5 存儲在hashSet中的元素為:
6 123

看了上面的代碼和運行結果,首先我們先了解一下什麽是hashCode?hashCode為什麽會被設計出來?或者它有什麽用處?
hashCode是jdk根據對象的地址或者字符串或者數字算出來的int類型的數值,public int hashCode()返回該對象的哈希碼值。本例中String中的hashCode()方法:

 1 public int hashCode() {
 2      int h = hash;
 3      if (h == 0 && value.length > 0) {
 4          char val[] = value;
 5 
 6          for (int i = 0; i < value.length; i++) {
 7              h = 31 * h + val[i];
 8          }
 9          hash = h;
10      }
11      return h;
12 }

那麽,hashCode值與equals是否有關系呢?答案是肯定的,如果使用equals()方法比較兩個對象得到true,那麽這兩個對象的hashCode必須是相同的。需要註意的是:這裏所指的是使用Object類中的equals()方法比較得到true。這也就要求了當繼承了Object的一個類需要重寫equals()方法來判斷相等邏輯時,也要同時重寫hashCode()方法來返回與equals()判斷邏輯一致的hashCode值。String類重寫了equals方法,所以當equals判斷相等時,必須返回給兩個對象相同的hashCode值。所以:上述代碼中s1和s2的hashCode均為48690。

hashCode的設計目的是為了提高哈希表的性能,那麽它是如何提高性能的呢?以上面代碼創建的hashSet為例,講述這個過程:

Hashset繼承了Set接口,在HashSet中不允許出現重復對象。在hashset中又是怎樣判定元素是否重復的呢?這就是問題的關鍵所在,在java的集合中,判斷兩個對象是否相等的規則是:

  1)先判斷兩個對象的hashCode是否相等,如果不相等,那麽就認為兩個對象不相等,就可以往HashSet中加入這兩個對象;如果hashCode相等,那麽要進行第二步;

  2)再使用equals方法判斷兩個對象相等,如果相等,則說明兩個對象相等,HashSet中不允許出現重復對象,例如上述代碼:即使顯示地給HashSet加入了s1和s2,但是我們發現遍歷結果並沒有輸出兩次“123”,僅有一次。

看到這裏,有的人可能會迷惑,在判斷對象是否相等時equals和hashCode哪個是主要判斷標準?很顯然是equals。因此總結equals()與hashCode的關系是:

1)hashCode相等的兩個對象,equals()返回的不一定是true。

2)equals()返回為true時,hashCode一定相同。

當HashSet中元素比較多,或者重寫equals()方法比較復雜時,每次往HashSet中加入一個元素,都要使用equals方法會使效率非常低,而直接先判斷hashCode是否相等,判斷hashCode是否相等就像一道堤壩先攔住了部分洪水,剩下來的洪水由另一個堤壩equals()攔截,大大提高了效率。

3.String類型傳參

先看一段代碼:

 1 public static void main(String[] args) {
 2     String s1 = "123";
 3     String s2 = new String("123");
 4     
 5     //輸出將s1、s2作為參數傳遞後的值
 6     changeString(s1);
 7     changeString(s2);
 8     System.out.println("將s1傳入changeString()方法後,s1:"+s1);
 9     System.out.println("將s2傳入changeString()方法後,s2:"+s2);
10 }
11 
12 //定義一個改變傳入參數(String類型)的方法
13 public static String changeString(String s) {
14     s = "我被改變了!";
15     return s;
16 }

運行結果:

1 將s1傳入changeString()方法後,s1:123
2 將s2傳入changeString()方法後,s2:123

運行結果告訴我們,盡管changeString()傳入的參數是String類型(對象類型),但是想通過此方法嘗試將s1,s2改變後,發現s1,s2並沒有發生變化。

Java中傳遞的永遠是值。我們知道,當傳入的參數是基本類型時,其實只是把值賦值給了形參,無論在方法體中如何對形參操作,原來的基本類型對應的值不會發生任何變化,比如:如下代碼

 1 public static void main(String[] args) {
 2     
 3     int a = 0;
 4     change(a);
 5     System.out.println("a經過change方法後,a仍然是:"+a);
 6 }
 7 
 8 public static int change(int a) {
 9     a = 666;
10     return a ;
11 }

只是將 0 賦值給了 形參a而已。

運行結果:

1 a經過change方法後,a仍然是:0

我們也知道,當傳入參數是對象類型時,相當於把對象的地址賦值給了形參,對形參進行操作即是對實參操作,實參會發生改變。如:

 1 public static void main(String[] args) {
 2     int[] a = new int[3];//定義一個長度為3的數組,數組為對象類型(引用類型)
 3     //為該數組中的每個元素賦值為1;
 4     for(int i =0;i<a.length;i++) {
 5         a[i] = 1;
 6     }
 7     
 8     System.out.println("a[]傳入change()方法前:");
 9     //遍歷數組中的元素
10     for(int i:a) {
11         System.out.println(i);
12     }
13     
14     change(a);
15     System.out.println("將a[]傳入change()方法後:");
16     //遍歷數組中的元素
17     for(int i:a) {
18         System.out.println(i);
19     }
20 }
21 
22 public static int[] change(int[]a) {
23     //為形參中的數組賦值為2;
24     for(int i=0;i<a.length;i++) {
25         a[i] = 2;
26     }
27     return a;
28 }

運行結果:

1 a[]傳入change()方法前:
2 1
3 1
4 1
5 將a[]傳入change()方法後:
6 2
7 2
8 2

那麽問題來了,同樣作為對象類型的String類對象,為什麽就不滿足當傳參是對象類型時的規則呢?請打開String類的API:

1 public final class String
2     implements java.io.Serializable, Comparable<String>, CharSequence {
3     ......
4     ......

我們可以發現,修飾String類的前面有個final關鍵字,該final關鍵字有什麽用?
用final修飾String類,表明String類是immutable(不可變的),當實例被創建時就會被初始化,並且無法修改實例信息。說點容易理解的:比如

當我們定義了:String s = "123"; 對s進行改變,將其改變為:s = "111"時,實際上並沒有在堆中修改原來s的值,而是重新指向一個新的對象和新的地址。如下圖:

技術分享圖片

所以,傳入參數是String類型時,在方法中對形參進行操作,與實參沒有關系,所以上述問題就迎刃而解了。

再放一張關於String常量池的圖:(體會區別)

技術分享圖片

好了 ,就這麽多。各位加油!                                     

2018/11/29 22:45:13

轉載請註明出處!

Java String引起的常量池、String類型傳參、“==”、“equals”、“hashCode”問題 細節分析