Java之字符串(String)全解析

字符串的本質

編程過程中,雖然字符串經常被像操作基本數據類型那樣來使用,但實質上任何編程語言都沒有提供字符串這種基本數據類型,字符串用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類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章