字符串的本質
編程過程中,雖然字符串經常被像操作基本數據類型那樣來使用,但實質上任何編程語言都沒有提供字符串這種基本數據類型,字符串用String類來表示。
String本身是一個類,與int,char等基本數據類型有本質的區別。只不過字符串在實際編程過程中使用的實在是非常頻繁,所以在Java裏面利用其JVM的支持提供了可以簡單使用Stirng類,使其可以像普通變量那樣採用直接賦值的方式進行字符串的定義。
字符串必須包含在一對雙引號(“ ”)之內,實際上描述的是一個String類的匿名對象。引申:因爲“ ”用來表示字符串,所以要在字符串中使用引號時則需要用到轉義字符\,在打包Json數據時會經常用到,Json數據中的key和value大多是字符串類型,例如:
String str = "\"version\": \"1.0.0\"";
String類之所以可以保存字符串的主要原因是其中定義了一個數組(byte類型數組),字符串中的每個字符數據都保存在了此數組之中,從 官方文檔 或Stirng類源代碼中可以看出。所以,所謂的字符串其實是對數組的一種包裝應用,那麼既然包裝的是數組,所以字符串裏面的內容是無法改變的。
字符串爲什麼不能改變
我們編程時,好像明明可以使用字符串拼接或者重新賦值來改變內容。觀察下面的代碼,如果可以通過“+”操作來改變字符串內容的話,應該打印true,但打印的是false。
String strA = "helloworld";
String strB = "hello";
String strC = strB + "world";
// false
System.out.println(strA == strC);
既然String類之中包含的是一個數組,數組的長度是固定的,那麼設置了一個字符串之後,會自動的進行一個數組空間的開闢,開闢內容的長度是固定的。事實上,不管是重新賦值還是使用“+”操作,都會在內存中創建新的字符串對象,所以代碼中strA和strB的地址是不相等的。在整個的處理過程中,字符串常量的內容沒有發生任何的改變,改變的只是一個String類對象的引用。
如果想要改變字符串的內容,可以使用StringBuffer類或StringBuilder類。
String對象(常量)池
String對象(常量)池的目的是實現數據的共享處理,在Java中對象(常量)池分兩種:
- 靜態常量池:程序(*.class)在加載的時候會自動將此程序之中保存的字符串、普通的常量、類和方法的信息等等靜態數據,全部進行分配
- 運行時常量池:當一個程序(*.class)加載之後,裏面可能有一些變量,這時候提供的常量池叫做運行時常量池
舉例:
String strA = "helloworld";
String strB = "hello" + "world";
String strC = "hello";
String strD = strC + "world";
// true
System.out.println(strA == strB);
// false
System.out.println(strA == strD);
上面代碼中,strA、strB和strC的內容會被放到靜態常量池,並且strA和strB的引用指向同一塊堆內存空間。strD的內容會被放到運行時常量池,因爲使用“+”進行字符串拼接時使用的是變量strC而不是“hello”,程序加載時並不能確定strC是什麼內容。
創建字符串
使用直接賦值的方式(推薦)
String str = "hello";
使用構造函數的方式
String str = new String("hello");
兩種方式的區別
- 使用直接賦值的方式,只會產生一個實例化對象,在有相同數據定義時可以減少對象的產生,實現數據的共享,以提升操作性能。
- 使用構造方法的方式,會產生兩個實例化對象,產生垃圾空間。
如下圖所示:第一段代碼,使用直接賦值的方式創建字符串時,會先到堆內存的字符串池中去查找是否已存在該數據,不存在時則創建,存在時則重用;第二段代碼,使用構造方法創建字符串時,會開闢兩塊堆內存空間,而實際只會用到一塊,另一塊由匿字符串常量"hello"創建的匿名對象將會變成垃圾空間。
空字符串
在字符串定義時,“""”和“null”不是一個概念,“""”表示有實例化對象,可以使用isEmpty()方法來判斷,“null”表示沒有實例化對象,使用isEmpty()方法時會報空指針異常。isEmpty()是用來判斷字符串的內容,所以一定要在有實例化對象的時候才能進行調用,equal()方法也是一樣的道理。
String str = “” 和 String str = null的區別
String str = “” 會在堆內存開闢空間,只不過存儲的內容是空的,String str = null則不會在堆內存開闢空間,只會在棧內存上創建String類對象的引用。
字符串比較
對int類型的比較使用 == ,字符串可以使用 == 進行比較,但得到的結果並非我們想要的,要比較兩個字符串的內容是否相等可以使用equal()方法。
“==”和equal()的區別
- “==”:進行的是數值上的比較,如果用在對象的比較上,比較的是兩個對象的地址的數值是否相等。
- equals():是類提供的一個比較方法,可以直接進行字符串的內容的判斷。注意:比較時區分大小寫。
引申內容
觀察下面兩段代碼,第一段是將字符串常量放到了括號內,第二段將字符串常量放到了前面(推薦)。
正常情況下,兩段代碼不會有什麼問題,但是,如果str是用戶輸入,並且用戶輸入了null,那麼第一段代碼會報空指針異常:Exception in thread “main” java.lang.NullPointerException,第二段則正常。原因:如果字符串常量寫到前面的話永遠不會出現空指針異常,字符串是一個匿名對象,匿名對象一定是開闢好堆內存空間的對象;而將字符串常量放到括號內,用戶輸入null時,相當於拿了一個沒有開闢內存空間的內容去做比較,所以會出現空指針異常。
String str = null;
//會出現空指針異常
if(str.equals("abc")){
System.out.println(str.length());
}
String str = null;
if("abc".equals(str)){
System.out.println(str.length());
}
常用字符串操轉換
先定義一個字符串:String str = “hello world”;
- 字符串轉字符數組
char[] c = str.toCharArray();
- 字符串轉大、小寫
String str2 = str.toUpperCase();
String str3 = str.toLowerCase();
- 字符串和字節數組相互轉換
byte[] bytes = str.getBytes();
String str2 = new String(bytes);
- 字符串轉int、double、float
String str = "123";
int i = Integer.parseInt(str);
String str2 = "12.3444411111";
Double d = Double.valueOf(str2);
Float f = Float.valueOf(str2);
- int、double、float轉字符串(一樣的操作)
int i = 123;
String str = String.valueOf(i);
常用字符串操作
先定義一個字符串:String str = “hello world”;
- 獲取指定位置的字符
char c = str.charAt(0);
- 獲取字符串長度
int len = str.length();
- 去除字符串中的空格
String str2 = str.trim();
注意:這裏只會去除掉字符串前方和尾部的空格,字符串中間的空格不會被去除。在驗證用戶輸入時應用較多。
- 字符串分隔
String[] str2 = str.split("l");
// 參數2表示分隔成兩部分
String[] str3 = str.split("l", 2);
注意:上面使用“l”對字符串進分隔,那麼生成的字符串數組中將不會在出現字符“l”。有時在傳輸數據時會以字符串的形式傳輸,多個數據之間會使用“,”隔開,解析時只要使用“,”進行分隔,就可以拿到我們想要的數據了。
- 判斷字符串是否存在
boolean isExistStr = str.contains("hello");
- 查找字符串的位置
// 返回-1時表示字符串不存在,也可用此方法來判斷字符串是否存在
int strLocation = str.indexOf("world");
- 實現字符串首字母轉大寫(String類沒有提供這個方法,但可以結合其它方法來實現)
class StringUtil{
public static String initcap(String str){
if (str == null || "".equals(null)){
return str;
}
if(str.length() == 1){
return str.toUpperCase();
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}
- 格式化輸出日期、時間
Date date = new Date();
// 2020-04-30
String data = String.format("%tF", date);
// 22:33:00
String time = String.format("%tT", date);
// 週四 4月 30 22:33:00 CST 2020
String dataAndTime = String.format("%tc",date);
String、StringBuffer、StringBuilder的區別
首先可以確認一點,String類是字符串的首選類型,絕大多數情況下使用String類已足夠。但是,創建成功的字符串,其內容是不能被改變的。雖然可以使用“+”來達到改變字符串的目的,但“+”會產生一個新的String實例,會在內存中創建新的字符串對象,如果重複的對字符串進行操作,將極大增加系統開銷。
StringBuffer類的append()方法則可以實現字符串內容的修改,並且不會生產新的對象,StringBuffer轉成String時直接調用toStirng()方法。
StringBuilder類的功能和StringBuffer類基本一致,兩者最大的區別在於StringBuffer類中的方法屬於線程安全的,全部使用了synchronized關鍵字進行了標註(從類的源代碼中可以看到),而StringBuilder類屬於非線程安全的。
下面的代碼顯示了頻繁修改字符串時,String類和StringBuilder類的效率:
String str = "";
long startTime = System.currentTimeMillis();
// str引用的指向在此處將被修改10000次,併產生大量垃圾空間
for(int i = 0; i < 10000; i++){
str = str + i;
}
long endTime = System.currentTimeMillis();
long time = endTime - startTime;
// 運行結果是122
System.out.println("String消耗時間:" + time);
StringBuilder stringBuilder = new StringBuilder("");
startTime = System.currentTimeMillis();
for(int i = 0; i < 10000; i++){
stringBuilder.append(i);
}
endTime = System.currentTimeMillis();
time = endTime - startTime;
// 運行結果是1
System.out.println("stringBuilder消耗時間:" + time);
如何選擇使用哪個類?
其實,根據三個類的特性來選擇就好。
絕大多數編程情況下會使用String類,String類的最大特點是其內容不允許修改。
StringBuilder類是非線程安全的,但也因此效率會高於StringBuffer類,在單線程的情況下,或者不存在共享數據的情況下可以使用StringBuilder類。
StringBuffer類是線程安全的,在擁有共享數據的多條線程並行工作的情況下,可以利用同步機制來保證數據的安全,可以說是以安全換時間,效率會低於StringBuilder類,多線程的情況下應該選擇
StringBuffer類。