前言
真的懂String么?真的懂String里面的==與equals的差別么?我想說原來可能我懂,但是后來就沒有后來了。。。
想要了解一個類,最好的辦法就是看這個類的實現源代碼,自己去編譯器里面去看吧。我就不粘貼代碼了。
一、String類
1)String類是final類,也即意味著String類不能被繼承,并且它的成員方法都默認為final方法。在Java中,被final修飾的類是不允許被繼承的,并且該類中的成員方法都默認為final方法。
2)String類其實是通過char數組來保存字符串的。
3)substring,concat,replace從源碼中的這三個方法可以看出,無論是substring、concat還是replace操作都不是在原有的字符串上進行的,而是重新生成了一個新的字符串對象
。也就是說進行這些操作后,最原始的字符串并沒有被改變。
在這里要永遠記住一點:“String對象一旦被創建就是固定不變的了,對String對象的任何改變都不影響到原對象,相關的任何change操作都會生成新的對象
”。
看下繼承結構源碼:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
..............
}
可以看到String是final的,不允許繼承.里面用來存儲value的是一個final數組,也是不允許修改的。
構造方法
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
...
可以看到默認的構造器是構建的空字符串,其實所有的構造器就是給value數組賦初值.
二、字符串常量池
我們知道字符串的分配和其他對象分配一樣,是需要消耗高昂的時間和空間的,而且字符串我們使用的非常多。JVM為了提高性能和減少內存的開銷,在實例化字符串的時候進行了一些優化:使用字符串常量池
。
每當我們創建字符串常量時,JVM會首先檢查字符串常量池,如果該字符串已經存在常量池中,那么就直接返回常量池中的實例引用。如果字符串不存在常量池中,就會實例化該字符串并且將其放到常量池中。由于String字符串的不可變性我們可以十分肯定常量池中一定不存在兩個相同的字符串(這點對理解上面至關重要)。
Java中的常量池,實際上分為兩種形態:靜態常量池和運行時常量池。
靜態常量池:即.class文件中的常量池,class文件中的常量池不僅僅包含字符串(數字)字面量,還包含類、方法的信息,占用class文件絕大部分空間。
運行時常量池:則是jvm虛擬機在完成類裝載操作后,將class文件中的常量池載入到內存中,并保存在方法區中,我們常說的常量池,就是指方法區中的運行時常量池
。
來看下面的程序:
String a = "yzzCool";
String b = "yzzCool";
a、b和字面上的yzzCool都是指向JVM字符串常量池中的"yzzCool"對象,他們指向同一個對象。
String c = new String("yzzCool");
new關鍵字一定會產生一個對象yzzCool(注意這個yzzCool和上面的yzzCool不同),同時這個對象是存儲在堆中。所以上面應該產生了兩個對象:保存在棧中的c和保存堆中abcdef。但是在Java中根本就不存在兩個完全一模一樣的字符串對象。故堆中的abcdef應該是引用字符串常量池中abcdef。所以c、abcdef、池abcdef的關系應該是:c--->yzzCool--->池yzzCool。整個關系如下:
圖水平一般,努力看還是能看清楚的???。
總結:雖然a、b、c、yzzCool是不同的對象,但是從String的內部結構我們是可以理解上面的。String c = new String("yzzCool");雖然c的內容是創建在堆中,但是他的內部value還是指向JVM常量池的yzzCool的value,它構造yzzCool時所用的參數依然是yzzCool字符串常量。
三、常見String面試題
3.1 String str = new String(“abc”)創建了多少個實例?
這個問題其實是不嚴謹的,但面試一般會遇到,所以我們要補充來說明。
類的加載和執行要分開來講:
創建了兩個。
1、當加載類時,”abc”被創建并駐留在了字符創常量池中(如果先前加載中沒有創建駐留 過)。
2、當執行此句時,因為”abc”對應的String實例已經存在于字符串常量池中,所以JVM會將此實例復制到會在堆(heap)中并返回引用地址。
3.2 JDK1.7的Intern的執行
/**
* 測試intern
*/
private static void testIntern() {
System.out.println("======================");
String s0 = "event";
String s1 = new String("event");
String s2 = new String("event");
s2 = s2.intern(); //把常量池中“event”的引用賦給s2
System.out.println(s0 == s1);//false
System.out.println(s0 == s1.intern());//true
System.out.println(s0 == s2);//true
System.out.println("======================");
String s11 = new String("look");
String s12 = s11.intern();
String s13 = "look";
System.out.println(s11 == s12);//false
System.out.println(s12 == s13);//true
System.out.println(s11 == s13);//false
System.out.println("======================");
String s3 = new String("abc") + new String("abc");
String s4 = "abcabc";
String s5 = s3.intern();
System.out.println(s5 == s3);//false
System.out.println(s5 == s4);//true
System.out.println(s3 == s4);//false
System.out.println("======================");
String s6 = new String("go") + new String("od");
String s7 = s6.intern();
String s8 = "good";
System.out.println(s6 == s7);//true
System.out.println(s7 == s8);//true
System.out.println(s6 == s8);//true
}
結果如下
false
true
true
======================
false
true
false
======================
false
true
false
======================
true
true
true
擴展1:String、StringBuffer、StringBuilder區別
StringBuffer、StringBuilder和String一樣,也用來代表字符串。
- String類是不可變類,任何對String的改變都 會引發新的String對象的生成;
- StringBuffer則是可變類,任何對它所指代的字符串的改變都不會產生新的對象。既然可變和不可變都有了,為何還有一個StringBuilder呢?相信初期的你,在進行append時,一般都會選擇StringBuffer吧!
先說一下集合的故事,HashTable是線程安全的,很多方法都是synchronized方法,而HashMap不是線程安全的,但其在單線程程序中的性能比HashTable要高。StringBuffer和StringBuilder類的區別也是如此,他們的原理和操作基本相同,區別在于StringBufferd支持并發操作,線性安全的,適 合多線程中使用。StringBuilder不支持并發操作,線性不安全的,不適合多線程中使用。新引入的StringBuilder類不是線程安全的,但其在單線程中的性能比StringBuffer高。
適合多線程使用的是StringBuffer,性能就沒有StringBuilder好,其他大致一樣
。
StringBuffer常用方法
由于StringBuffer和StringBuilder在使用上幾乎一樣,所以只寫一個,以下部分內容網絡各處收集,不再標注出處
1.初始化;
StringBuffer s = new StringBuffer();
這樣初始化出的StringBuffer對象是一個空的對象,
StringBuffer sb1=new StringBuffer(512);
分配了長度512字節的字符緩沖區。
StringBuffer sb2=new StringBuffer(“how are you?”)
創建帶有內容的StringBuffer對象,在字符緩沖區中存放字符串“how are you?”
2.append()追加
public StringBuffer append(boolean b)
append里面的參數是多種多樣啊!
該方法的作用是追加內容到當前StringBuffer對象的末尾,類似于字符串的連接,調用該方法以后,StringBuffer對象的內容也發生改 變,例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.append(true);
則對象sb的值將變成”abctrue”
使用該方法進行字符串的連接,將比String更加節約內容,經常應用于數據庫SQL語句的連接。
3.deleteCharAt(int index)刪除
public StringBuffer deleteCharAt(int index)
該方法的作用是刪除指定位置的字符,然后將剩余的內容形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“KMing”);
sb. deleteCharAt(1);
該代碼的作用刪除字符串對象sb中索引值為1的字符,也就是刪除第二個字符,剩余的內容組成一個新的字符串。所以對象sb的值變 為”King”。
如果是這么寫:
StringBuffer sb = new StringBuffer(“KMing”);
StringBuffer sb1 = sb. deleteCharAt(1);
你會發現sb和sb1的值是一樣的。
還存在一個功能類似的delete方法:
public StringBuffer delete(int start,int end)
該方法的作用是刪除指定區間以內的所有字符,包含start,不包含end索引值的區間。例如:
StringBuffer sb = new StringBuffer(“TestString”);
sb. delete (1,4);
該代碼的作用是刪除索引值1(包括)到索引值4(不包括)之間的所有字符,剩余的字符形成新的字符串。則對象sb的值是”TString”。
4.insert(int offset, Object obj)插入
public StringBuffer insert(int offset, Object obj)
該方法的作用是在StringBuffer對象中插入內容,然后形成新的字符串。第二個參數可以是好多形式,下面以boolean為例子。
StringBuffer sb = new StringBuffer(“TestString”);
sb.insert(4,false);
該示例代碼的作用是在對象sb的索引值4的位置插入false值,形成新的字符串,則執行以后對象sb的值是”TestfalseString”。
5.reverse()反轉
public StringBuffer reverse()
該方法的作用是將StringBuffer對象中的內容反轉,然后形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.reverse();
經過反轉以后,對象sb中的內容將變為”cba”。
6.setCharAt(int index, char ch)插入字符
public void setCharAt(int index, char ch)
該方法的作用是修改對象中索引值為index位置的字符為新的字符ch。例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.setCharAt(1,’D’);
則對象sb的值將變成”aDc”。
7.trimToSize()縮小存儲空間
public void trimToSize()
該方法的作用是將StringBuffer對象的中存儲空間縮小到和字符串長度一樣的長度,減少空間的浪費,和String的trim()是一樣的作用,不在舉例。
8.length()得到字符的長度
public void setLength(int newLength)
該方法的作用是設置字符串緩沖區大小。
StringBuffer sb=new StringBuffer();
sb.setlength(100);
如果用小于當前字符串長度的值調用setlength()方法,則新長度后面的字符將丟失。
意思就是:我們現在的StringBuffer的長度是120,如果我們設置setLength(100),則我們的StringBuffer最終的長度是100,如果是提前運行setLength(100),然后進行append()操作,則結果是是多少就是多少。
9.capacity()得到字符串容量的大小
public int capacity()
該方法的作用是獲取字符串的容量。
StringBuffer sb=new StringBuffer(“string”);
int i=sb.capacity();
10.ensureCapacity(int minimumCapacity)設置字符串的容量。
public void ensureCapacity(int minimumCapacity)
該方法的作用是重新設置字符串容量的大小。
StringBuffer sb=new StringBuffer();
sb.ensureCapacity(32); //預先設置sb的容量為32
注意:我們來分析一下capacity()和ensureCapacity(int minimumCapacity),如果我們設置ensureCapacity(30),但是我們得到的capacity()的值并不是30,這是什么玩意???下面自己的理解:這里設置容量就是提前告訴字符串,我會達到這個水平,有一個明顯的現象就是字符串有一個默認的容量是16,當目前的容量小于16的時候,用capacity()得到的值都是16,當容量大于等于16時,要擴容了擴容的原則是“目前的容量+2”就是16+16+2=34,容量在16~34之間時,capacity()得到的值都是34..........以后根據這個原則擴容。
又有問題了,我的容量不是16,34,80.....這是什么情況??嘿嘿,是這樣的如果你在新建StringBuffer的時候是這樣寫的,如下:
StringBuffer buffer1 = new StringBuffer("1234567890");
那么他的初始容量不是16,是“16+字符串的長度”,也就是上面的容量是10+16=26;那么它的規則就是26,54,110.........
11.getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)處理字符串的子字符串復制給數組。
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
該方法的作用是將字符串的子字符串復制給數組。
StringBuffer sb = new StringBuffer("I love You");
int begin = 0;
int end = 5;
//注意ch字符數組的長度一定要大于等于begin到end之間字符的長度
//小于的話會報ArrayIndexOutOfBoundsException
//如果大于的話,大于的字符會以空格補齊
char[] ch = new char[end-begin];
sb.getChars(begin, end, ch, 0);
System.out.println(ch);
結果:I lov
參數的意義:srcBegin:從字符串的第幾位開始,(包括這一位);
srcEnd:從字符串的第幾位開始,(不包括這一位);
dst:要操作的字符串數組;
dstBegin:從數組的第幾個下表開始處理。
注意:
上面的解釋如果感覺不是很詳細可以看Java中String的實現與應用這篇博客。