Java常用API(六)
9.6.3 字符串常量池
1、字符串常量對(duì)象可以共享的原因和好處
字符串常量對(duì)象可以共享的原因:字符串對(duì)象不可變
字符串常量對(duì)象共享的好處:節(jié)省內(nèi)存
String s1 = "atguigu";
String s2 = "atguigu";
System.out.println(s1 == s2);//這里只創(chuàng)建了一個(gè)字符串對(duì)象"atguigu"。
s2 = s2.replace("a","o");
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
這里s1指向的"atguigu"和s2指向的"atguigu"是同一個(gè)。
如果無(wú)法保證"atguigu"對(duì)象不可變,那么當(dāng)s2將"a"替換為"o"之后,那么s1就會(huì)受到影響,這樣是不安全的。
但是,現(xiàn)在我們發(fā)現(xiàn)s1并未受到影響,也就是說(shuō),s1指向的"atguigu"對(duì)象并未被修改,而是基于"atguigu"重新復(fù)制了一個(gè)新對(duì)象"atguigu",然后替換成"otguigu"。
2、hashCode方法
Object類(lèi)有一個(gè)int hashCode()方法,該方法用于計(jì)算對(duì)象的哈希值。哈希值的作用就好比生活中的身份證號(hào),用一串?dāng)?shù)字代表一個(gè)對(duì)象。哈希值的計(jì)算是有講究的,按照常規(guī)協(xié)定hashCode方法和equals方法要一起重寫(xiě),要求兩個(gè)“相等”的對(duì)象hashCode必須相同,如果兩個(gè)對(duì)象的哈希值不同,它倆調(diào)用equals方法也必須是false,但是如果兩個(gè)對(duì)象的哈希值相同,它倆調(diào)用equals方法卻不一定true。
字符串對(duì)象也重寫(xiě)了hashCode方法,String類(lèi)的hashCode值計(jì)算規(guī)則如下:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
再好的哈希值計(jì)算規(guī)則也很難保證,兩個(gè)“不相等”對(duì)象的哈希值一定不同。例如:
String s3 = "Aa";//2112
String s4 = "BB";//2112
3、字符串常量池
字符串常量池是一個(gè)哈希表,它里面記錄了可以共享使用的字符串常量對(duì)象的地址。采用哈希表結(jié)構(gòu)的目的是為了提高性能,用空間換時(shí)間。字符串對(duì)象的地址散列存儲(chǔ)在哈希表中,雖然是散列存儲(chǔ),但是因?yàn)榭梢允褂米址畬?duì)象的hashCode值快速的計(jì)算存儲(chǔ)位置的下標(biāo),所以效率還是很高的。
String s1 = "hello";
String s2 = "hello";
當(dāng)給s1賦值"hello"時(shí),根據(jù)"hello"的hashCode()值,計(jì)算出來(lái)index=[2],如果table[index]=table[2]=null,那就把"hello"對(duì)象的字符串的地址0x7534放到table[2]中。
當(dāng)給s2賦值"hello"時(shí),根據(jù)"hello"的hashCode()值,計(jì)算出來(lái)index=[2],此時(shí)table[index]=table[2]!=null,那就直接把"hello"的內(nèi)存地址0x7534賦值給s2。
String s3 = "Aa";
String s4 = "BB";
當(dāng)給s3賦值"Aa"時(shí),根據(jù)"Aa"的hashCode()值,計(jì)算出來(lái)index=[6],如果table[index]=table[6]=null,那就把"Aa"對(duì)象的字符串的地址0x8989放到table[6]中。
當(dāng)給s4賦值"BB"時(shí),根據(jù)"BB"的hashCode()值,計(jì)算出來(lái)index=[6],此時(shí)table[index]=table[6]!=null,但是"BB"和"Aa"不一樣,那就直接把"BB"的內(nèi)存地址0x6666也放到table[6]中,相當(dāng)于table[6]中記錄了兩個(gè)字符串對(duì)象的地址,它們使用鏈表連接起來(lái)。
4、哪些字符串對(duì)象地址放入字符串常量池?
需要共享的字符串地址記錄到字符串常量池的table表中,不需要共享的字符串對(duì)象其地址值不需要記錄到字符串常量池的table表中。除了以下2種,其他的都不放入字符串常量池:
(1)""直接的字符串 (備注:兩個(gè)""的字符串直接+,編譯器處理成一個(gè)""字符串)
(2)字符串對(duì)象.intern()結(jié)果
其他:
(1)直接new
(2)valueOf,copyValueOf等
(3)字符串對(duì)象拼接:concat拼接 以及 字符串變量 + 拼接
(4)toUpperCase,toLowerCase,substring,repalce等各種String方法得到的字符串
這些方式,本質(zhì)都是新new的。
package com.atguigu.string;
import org.junit.Test;
public class TestStringTable {
@Test
public void test1(){
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
}
@Test
public void test2(){
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s1 == s2);//false
System.out.println(s3 == s4);//true
}
@Test
public void test3(){
String s1 = "hello";
String s2 = "world";
String s3 = "HELLOWORLD";
String s4 = s1.concat(s2);
String s5 = s1 + s2;
String s6 = s3.toLowerCase();
String s7 = "hello" + "world";
String s8 = "helloworld";
System.out.println(s4 == s8);//false
System.out.println(s5 == s8);//false
System.out.println(s6 == s8);//false
System.out.println(s7 == s8);//true
}
}
5、字符串對(duì)象和字符串常量池在哪里?
字符串常量池表:
- JDK1.6:在方法區(qū)的永久代
- JDK1.7之后:堆
字符串對(duì)象:
- JDK1.7之前:需要共享的字符串對(duì)象存儲(chǔ)在方法區(qū)的永久代,然后把對(duì)象地址記錄到字符串常量池的table表中,不需要共享的字符串對(duì)象存儲(chǔ)在堆中,其地址值不需要記錄到字符串常量池的table表中。
- JDK1.7之后:所有字符串對(duì)象都存儲(chǔ)在堆中。同樣需要共享的字符串地址記錄到字符串常量池的table表中,不需要共享的字符串對(duì)象其地址值不需要記錄到字符串常量池的table表中。
字符串的intern方法:
當(dāng)調(diào)用intern方法時(shí),如果池已經(jīng)包含一個(gè)等于此String對(duì)象的字符串,則返回池中的字符串。否則,將此String對(duì)象添加到池中,并返回此String對(duì)象的引用。
package com.atguigu.string;
import org.junit.Test;
public class TestStringIntern {
@Test
public void test1(){
String s1 = new String("hello");
String s2 = s1.intern();
System.out.println(s1 == s2);
/*
JDK8:false
JDK6:false
*/
}
@Test
public void test2(){
String s1 = "he".concat("llo");
String s2 = s1.intern();
System.out.println(s1 == s2);
/*
JDK8:true
JDK6:false
*/
}
}
6、字符串的對(duì)象的個(gè)數(shù)(面試題)
String str1 = "hello";
String str2 = new String("hello");
//上面的代碼一共有幾個(gè)字符串對(duì)象。
//2個(gè)
String s1 = new String("hello");
String s2 = s1.intern();
//JDK1.6,2個(gè)
//JDK1.8,2個(gè)
String s = "a" + "b" + "c" +"d";
//1個(gè),abcd
String s1 = "he".concat("llo");
String s2 = s1.intern();
//JDK1.6 4個(gè)
//JDK1.8 3個(gè)
9.6.4 字符串對(duì)象的內(nèi)存分析
就算不共享同一個(gè)字符串對(duì)象,字符串對(duì)象之間也會(huì)“盡量”共享同一個(gè)value數(shù)組。
package com.atguigu.string;
import org.junit.Test;
public class TestStringMemory {
@Test
public void test1(){
String str1 = new String("hello");
System.out.println(str1.hashCode());
char[] arr = {'h','e','l','l','o'};
String str2 = new String(arr);
}
}
9.7 可變字符序列
9.7.1 String與可變字符序列的區(qū)別
因?yàn)镾tring對(duì)象是不可變對(duì)象,雖然可以共享常量對(duì)象,但是對(duì)于頻繁字符串的修改和拼接操作,效率極低。因此,JDK又在java.lang包提供了可變字符序列StringBuilder和StringBuffer類(lèi)型。
StringBuffer:老的,線(xiàn)程安全的(因?yàn)樗姆椒ㄓ衧ynchronized修飾)
StringBuilder:線(xiàn)程不安全的
StringBuilder類(lèi)底層是char[]數(shù)組存儲(chǔ),默認(rèn)初始化長(zhǎng)度16,每次增長(zhǎng)是原長(zhǎng)度 * 2 + 2
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0)
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
JDK9之后,StringBuilder類(lèi)底層是byte[]數(shù)組存儲(chǔ),默認(rèn)長(zhǎng)度和JDK8不同,有所改變
- new StringBuilder()源碼
//StringBuilder的父類(lèi)構(gòu)造方法
//StringBuilder(){super(16)}
AbstractStringBuilder(int capacity) {
//如果為true,判斷為沒(méi)有中文字符
if (COMPACT_STRINGS) {
//數(shù)組value = new byte[16]
value = new byte[capacity];
//coder = 0
coder = LATIN1;
} else {
//判斷有中文字符
//value數(shù)組 = newBytesFor方法的返回值
value = StringUTF16.newBytesFor(capacity);
//coder = 1
coder = UTF16;
}
}
//返回字節(jié)數(shù)組
public static byte[] newBytesFor(int len) {
if (len < 0) {
throw new NegativeArraySizeException();
}
if (len > MAX_LENGTH) {
throw new OutOfMemoryError("UTF16 String size is " + len +
", should be less than " + MAX_LENGTH);
}
//數(shù)組長(zhǎng)度為 16 << 1 = 32
return new byte[len << 1];
}
構(gòu)建StringBuilder對(duì)象,此時(shí)COMPACT_STRINGS變量為true,數(shù)組長(zhǎng)度為16
- new StringBuilder("abc")源碼
AbstractStringBuilder(String str) {
//獲取長(zhǎng)度 lenght = 3
int length = str.length();
//數(shù)組容量 = 19
int capacity = (length < Integer.MAX_VALUE - 16)
? length + 16 : Integer.MAX_VALUE;
// initCoder = 0 無(wú)中文字符
final byte initCoder = str.coder();
coder = initCoder;
//value數(shù)組創(chuàng)建后的長(zhǎng)度為19
value = (initCoder == LATIN1)
? new byte[capacity] : StringUTF16.newBytesFor(capacity);
append(str);
}
- new StringBuilder("你好")源碼
AbstractStringBuilder(String str) {
//獲取長(zhǎng)度 lenght = 2
int length = str.length();
//數(shù)組容量 = 18
int capacity = (length < Integer.MAX_VALUE - 16)? length + 16 : Integer.MAX_VALUE;
// initCoder = 1 有中文字符
final byte initCoder = str.coder();
coder = initCoder;
//value數(shù)組創(chuàng)建后的長(zhǎng)度為36
value = (initCoder == LATIN1)? new byte[capacity] : StringUTF16.newBytesFor(capacity);
//(interCoder == LATIN1) 結(jié)果為false,執(zhí)行 StringUTF16.newBytesFor
append(str);
}
9.7.2 StringBuilder、StringBuffer的API
常用的API,StringBuilder、StringBuffer的API是完全一致的
- StringBuffer append(xx):拼接,追加
- StringBuffer insert(int index, xx):在[index]位置插入xx
- StringBuffer delete(int start, int end):刪除[start,end)之間字符
- StringBuffer deleteCharAt(int index):刪除[index]位置字符
- void setCharAt(int index, xx):替換[index]位置字符
- StringBuffer reverse():反轉(zhuǎn)
- void setLength(int newLength) :設(shè)置當(dāng)前字符序列長(zhǎng)度為newLength
- StringBuffer replace(int start, int end, String str):替換[start,end)范圍的字符序列為str
- int indexOf(String str):在當(dāng)前字符序列中查詢(xún)str的第一次出現(xiàn)下標(biāo) int indexOf(String str, int fromIndex):在當(dāng)前字符序列[fromIndex,最后]中查詢(xún)str的第一次出現(xiàn)下標(biāo)
- int lastIndexOf(String str):在當(dāng)前字符序列中查詢(xún)str的最后一次出現(xiàn)下標(biāo) int lastIndexOf(String str, int fromIndex):在當(dāng)前字符序列[fromIndex,最后]中查詢(xún)str的最后一次出現(xiàn)下標(biāo)
- String substring(int start):截取當(dāng)前字符序列[start,最后] String substring(int start, int end):截取當(dāng)前字符序列[start,end)
- String toString():返回此序列中數(shù)據(jù)的字符串表示形式
- void trimToSize():嘗試減少用于字符序列的存儲(chǔ)空間。如果緩沖區(qū)大于保存當(dāng)前字符序列所需的存儲(chǔ)空間,則將重新調(diào)整其大小,以便更好地利用存儲(chǔ)空間。
@Test
public void test6(){
StringBuilder s = new StringBuilder("helloworld");
s.setLength(30);
System.out.println(s);
}
@Test
public void test5(){
StringBuilder s = new StringBuilder("helloworld");
s.setCharAt(2, 'a');
System.out.println(s);
}
@Test
public void test4(){
StringBuilder s = new StringBuilder("helloworld");
s.reverse();
System.out.println(s);
}
@Test
public void test3(){
StringBuilder s = new StringBuilder("helloworld");
s.delete(1, 3);
s.deleteCharAt(4);
System.out.println(s);
}
@Test
public void test2(){
StringBuilder s = new StringBuilder("helloworld");
s.insert(5, "java");
s.insert(5, "chailinyan");
System.out.println(s);
}
@Test
public void test1(){
StringBuilder s = new StringBuilder();
s.append("hello").append(true).append('a').append(12).append("atguigu");
System.out.println(s);
System.out.println(s.length());
}
9.7.3 效率測(cè)試
package com.atguigu.stringbuffer;
import org.junit.Test;
public class TestTime {
@Test
public void testString(){
long start = System.currentTimeMillis();
String s = new String("0");
for(int i=1;i<=10000;i++){
s += i;
}
long end = System.currentTimeMillis();
System.out.println("String拼接+用時(shí):"+(end-start));//367
long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("String拼接+memory占用內(nèi)存: " + memory);//473081920字節(jié)
}
@Test
public void testStringBuilder(){
long start = System.currentTimeMillis();
StringBuilder s = new StringBuilder("0");
for(int i=1;i<=10000;i++){
s.append(i);
}
long end = System.currentTimeMillis();
System.out.println("StringBuilder拼接+用時(shí):"+(end-start));//5
long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("StringBuilder拼接+memory占用內(nèi)存: " + memory);//13435032
}
@Test
public void testStringBuffer(){
long start = System.currentTimeMillis();
StringBuffer s = new StringBuffer("0");
for(int i=1;i<=10000;i++){
s.append(i);
}
long end = System.currentTimeMillis();
System.out.println("StringBuffer拼接+用時(shí):"+(end-start));//5
long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("StringBuffer拼接+memory占用內(nèi)存: " + memory);//13435032
}
}
9.8 新特性:文本塊
在Java中,通常需要使用String類(lèi)型表達(dá)HTML,XML,SQL或JSON等格式的字符串,在進(jìn)行字符串賦值時(shí)需要進(jìn)行轉(zhuǎn)義和連接操作,然后才能編譯該代碼,這種表達(dá)方式難以閱讀并且難以維護(hù)。而有了文本塊以后,用戶(hù)不需要轉(zhuǎn)義,Java能自動(dòng)搞定。因此,**文本塊將提高Java程序的可讀性和可寫(xiě)性。**
JDK 12引入了Raw String Literals特性,但在其發(fā)布之前就放棄了這個(gè)特性。這個(gè)JEP與引入多行字符串文字(文本塊)在意義上是類(lèi)似的。Java 13中引入了文本塊(預(yù)覽特性),這個(gè)新特性跟Kotlin中的文本塊是類(lèi)似的。
Java 14給文本塊引入了兩個(gè)新的轉(zhuǎn)義序列。一是可以使用新的\s轉(zhuǎn)義序列來(lái)表示一個(gè)空格;二是可以使用反斜杠“\”來(lái)避免在行尾插入換行字符,這樣可以很容易地在文本塊中將一個(gè)很長(zhǎng)的行分解成多行來(lái)增加可讀性。
預(yù)覽的新特性文本塊在Java 15中被最終確定下來(lái),Java 15之后我們就可以放心使用該文本塊了。
**舉例**
如有一段以下字符串:
Hello, 尚硅谷
將其復(fù)制到Java的字符串中,會(huì)展示成以下內(nèi)容:
"\n" +
" \n" +
" Hello, 尚硅谷\n" +
" \n" +
"\n";
即被自動(dòng)進(jìn)行了轉(zhuǎn)義,這樣的字符串看起來(lái)不是很直觀,在文本塊的新特性中,就可以使用以下語(yǔ)法了:
"""
Hello, world
""";
使用"""作為文本塊的開(kāi)始符和結(jié)束符,在其中就可以放置多行的字符串,不需要進(jìn)行任何轉(zhuǎn)義??雌饋?lái)就十分清爽了。
注意:
- 開(kāi)始分隔符由三個(gè)雙引號(hào)字符表示,后面只能跟零個(gè)或多個(gè)空格,最終以行終止符結(jié)束。
- 文本塊內(nèi)容以開(kāi)始分隔符的行終止符后的第一個(gè)字符開(kāi)始,到結(jié)束分隔符的第一個(gè)雙引號(hào)之前的最后一個(gè)字符結(jié)束。
以下示例代碼是錯(cuò)誤格式的文本塊:
String err1 = """""";//開(kāi)始分隔符后沒(méi)有行終止符
String err2 = """ """;//開(kāi)始分隔符后沒(méi)有行終止符
String err3 = """ abc
"""; //開(kāi)始分隔符后除了空格之外還有其他字符,然后才是行終止符
如果要表示空字符串需要以下示例代碼表示:
String emp1 = "";//推薦
String emp2 = """
""";//第二種需要兩行,更麻煩了
案例:
public class TestStringBlock {
public static void main(String[] args) {
String htmlStr = """
Hello, world
""";
System.out.println(htmlStr);
String story = """
Elly said,"Maybe I was a bird in another life."
Noah said,"If you're a bird, I'm a bird."
""";
System.out.println(story);
String text = """
\s\s人最寶貴的東西是生命,生命對(duì)人來(lái)說(shuō)只有一次。\
因此,人的一生應(yīng)當(dāng)這樣度過(guò):當(dāng)一個(gè)人回首往事時(shí),\
不因虛度年華而悔恨,也不因碌碌無(wú)為而羞愧;\
這樣,在他臨死的時(shí)候,能夠說(shuō),\
我把整個(gè)生命和全部精力都獻(xiàn)給了人生最寶貴的事業(yè)\
——為人類(lèi)的解放而奮斗。
""";
System.out.println(text);
}
}