String中==和equals分別比較的是什麼?
首先先了解一下java中的==和equals分別比較的是什麼?
對於基本的資料型別來說:==比較的是兩個基本型別的值。
對於複合資料型別來說:==和equals比較的都是物件在記憶體中存放地址(確切的 說是堆記憶體地址)。
因此對於String,Integer,Date等覆蓋了equals方法的型別,==比較的是存放的記憶體地址。而equals比較的是具體覆蓋後的程式碼。
String中equals比較的兩個字串內容是否相等。
public boolean equals(Object anObject){
if(this==anObject){
return true;
}
if(anObject instanceof String){
//如果資料型別為String,則比較char陣列內的每個字串的值是否==
String anotherString = (String)anObject;
int n = value.length;
if(n == anotherString.value.length){
char v1[] = value;
char v2[] = anotherString.value;
int i=0;
while(n--!=0){
if(v1[i]!=v2[i])
return false; i++;
} return true;
}
}
return false;
}
然後我們來看一下String是如何賦值的?
String有兩種賦值的方式:
1、像普通物件使用new的方法:String str= new String(“abcd”);
2、像基本資料型別的方法: String str =”abcd”;
那麼為什麼String 可以不用new的方式來賦值呢?
這是因為編譯器起到了作用:當你對“abcd”沒有使用new的時候,編譯器會預設呼叫構造器new String(char value[])。
因此顯而易見隱式情況下,呼叫了String的建構函式。
那隱式和顯示賦值的方法有何不同?
隱式:
String str =“ok”;
利用了字串快取池,也就說如果快取池中已經存在了相同的字串,就不會產生新的物件,
而是直接返回了快取池中的字串物件的引用;
如:
String a="ok";//新建一個String物件
String b ="ok";//從快取池中找
System.out.println(a == b);//true
String c = new String("ok");//新建一個String物件
String d = new String("ok");//新建一個String物件
System.out.println(c == d);//false
這時比較的話可以使用equals就相等了。
下面拓展一個方法:intern()
1.為什麼要介紹intern()方法
intern()方法設計的初衷,就是重用String物件,以節省記憶體消耗。這麼說可能有點抽象,那麼就用例子來證明。
static final int MAX = 100000;
static final String[] arr = new String[MAX];
public static void main(String[] args) throws Exception {
//為長度為10的Integer陣列隨機賦值
Integer[] sample = new Integer[10];
Random random = new Random(1000);
for (int i = 0; i < sample.length; i++) {
sample[i] = random.nextInt();
}
//記錄程式開始時間
long t = System.currentTimeMillis();
//使用/不使用intern方法為10萬個String賦值,值來自於Integer陣列的10個數
for (int i = 0; i < MAX; i++) {
arr[i] = new String(String.valueOf(sample[i % sample.length]));
//arr[i] = new String(String.valueOf(sample[i % sample.length])).intern();
}
System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
}
這個例子也比較簡單,就是為了證明使用intern()比不使用intern()消耗的記憶體更少。
先定義一個長度為10的Integer陣列,並隨機為其賦值,在通過for迴圈為長度為10萬的String物件依次賦值,這些值都來自於Integer陣列。兩種情況分別執行,可通過Window
—> Preferences –> Java –> Installed JREs設定JVM啟動引數為-agentlib:hprof=heap=dump,format=b,將程式執行完後的hprof置於工程目錄下。再通過MAT外掛檢視該hprof檔案。
兩次實驗結果如下:
從執行結果來看,不使用intern()的情況下,程式生成了101762個String物件,而使用了intern()方法時,程式僅生成了1772個String物件。自然也證明了intern()節省記憶體的結論。
細心的同學會發現使用了intern()方法後程序執行時間有所增加。這是因為程式中每次都是用了new String後又進行intern()操作的耗時時間,但是不使用intern()佔用記憶體空間導致GC的時間是要遠遠大於這點時間的。
2.深入認識intern()方法
JDK1.7後,常量池被放入到堆空間中,這導致intern()函式的功能不同,具體怎麼個不同法,且看看下面程式碼,這個例子是網上流傳較廣的一個例子,分析圖也是直接貼上過來的,這裡我會用自己的理解去解釋這個例子:
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
輸出結果為:
JDK1.6以及以下:false false
JDK1.7以及以上:false true
再分別調整上面程式碼2.3行、7.8行的順序:
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
輸出結果為:
JDK1.6以及以下:false false
JDK1.7以及以上:false false
下面依據上面程式碼對intern()方法進行分析:
2.1 JDK1.6
在JDK1.6中所有的輸出結果都是 false,因為JDK1.6以及以前版本中,
常量池是放在 Perm 區(屬於方法區)中的,熟悉JVM的話應該知道這是和堆區完全分開的。
使用引號宣告的字串都是會直接在字串常量池中生成的,而 new 出來的 String 物件是放在堆空間中的。所以兩者的記憶體地址肯定是不相同的,即使呼叫了intern()方法也是不影響的。
intern()方法在JDK1.6中的作用是:
比如String s = new String(“SEU_Calvin”),再呼叫s.intern(),此時返回值還是字串”SEU_Calvin”,表面上看起來好像這個方法沒什麼用處。
但實際上,在JDK1.6中它做了個小動作:檢查字串池裡是否存在”SEU_Calvin”這麼一個字串,如果存在,就返回池裡的字串;如果不存在,該方法會把”SEU_Calvin”新增到字串池中,然後再返回它的引用。然而在JDK1.7中卻不是這樣的。
2.2 JDK1.7
針對JDK1.7以及以上的版本,我們將上面兩段程式碼分開討論。先看第一段程式碼的情況:
再把第一段程式碼貼一下便於檢視:
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
String s = newString(“1”),生成了常量池中的“1” 和堆空間中的字串物件。
s.intern(),這一行的作用是s物件去常量池中尋找後發現”1”已經存在於常量池中了。 String s2 =
“1”,這行程式碼是生成一個s2的引用指向常量池中的“1”物件。 結果就是 s 和 s2 的引用地址明顯不同。因此返回了false。
String s3 = new String(“1”) + newString(“1”),這行程式碼在字串常量池中生成“1” ,並在堆空間中生成s3引用指向的物件(內容為”11”)。注意此時常量池中是沒有 “11”物件的。
s3.intern(),這一行程式碼,是將 s3中的“11”字串放入 String 常量池中,此時常量池中不存在“11”字串,JDK1.6的做法是直接在常量池中生成一個 “11” 的物件。
但是在JDK1.7中,常量池中不需要再儲存一份物件了,可以直接儲存堆中的引用。這份引用直接指向 s3 引用的物件,也就是說s3.intern() ==s3會返回true。
String s4 = “11”, 這一行程式碼會直接去常量池中建立,但是發現已經有這個物件了,此時也就是指向 s3 引用物件的一個引用。因此s3 == s4返回了true。
下面繼續分析第二段程式碼:
再把第二段程式碼貼一下便於檢視:
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
String s = newString(“1”),生成了常量池中的“1” 和堆空間中的字串物件。 String s2 =
“1”,這行程式碼是生成一個s2的引用指向常量池中的“1”物件,但是發現已經存在了,那麼就直接指向了它。
s.intern(),這一行在這裡就沒什麼實際作用了。因為”1”已經存在了。 結果就是 s 和 s2
的引用地址明顯不同。因此返回了false。String s3 = new String(“1”) + newString(“1”),這行程式碼在字串常量池中生成“1”
,並在堆空間中生成s3引用指向的物件(內容為”11”)。注意此時常量池中是沒有 “11”物件的。String s4 = “11”;
這一行程式碼會直接去生成常量池中的”11”。 s3.intern(),這一行在這裡就沒什麼實際作用了。因為”11”已經存在了。 結果就是 s3
和 s4 的引用地址明顯不同。因此返回了false。
3 總結
現在再來看一下開篇給的引入例子,是不是就很清晰了呢。
String str1 = new String("SEU") + new String("Calvin");
System.out.println(str1.intern() == str1);
System.out.println(str1 == "SEUCalvin");
str1.intern() == str1就是上面例子中的情況,str1.intern()發現常量池中不存在“SEUCalvin”,因此指向了str1。 “SEUCalvin”在常量池中建立時,也就直接指向了str1了。兩個都返回true就理所當然啦。
那麼第二段程式碼呢:
String str2 = "SEUCalvin";//新加的一行程式碼,其餘不變
String str1 = new String("SEU")+ new String("Calvin");
System.out.println(str1.intern() == str1);
System.out.println(str1 == "SEUCalvin");
也很簡單啦,str2先在常量池中建立了“SEUCalvin”,那麼str1.intern()當然就直接指向了str2,你可以去驗證它們兩個是返回的true。後面的”SEUCalvin”也一樣指向str2。所以誰都不搭理在堆空間中的str1了,所以都返回了false。