Java String引起的常量池、String類型傳參、“==”、“equals”、“hashCode”問題 細節分析
在學習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”問題 細節分析