String

CH_song發表於2024-08-18

遞迴

(菲波那切數列)

遞迴:方法自己呼叫自己的現象就稱為遞迴。

遞迴的分類:

  • 遞迴分為兩種,直接遞迴和間接遞迴。
  • 直接遞迴稱為方法自身呼叫自己。
  • 間接遞迴可以A方法呼叫B方法,B方法呼叫C方法,C方法呼叫A方法。

注意事項

  • 遞迴一定要有條件限定,保證遞迴能夠停止下來,否則會發生棧記憶體溢位。

  • 在遞迴中雖然有限定條件,但是遞迴深度不能太深,否則效率低下,或者也會發生棧記憶體溢位。

    • 能夠使用迴圈代替的,儘量使用迴圈代替遞迴

字串String

java.lang.String 類代表字串。Java語言提供物件字串的特殊支援:

  • Java程式中所有的字串文字(例如"abc" )都可以被看作是實現此類的例項,即所有""引起來的內容都是字串物件
  • Java語言提供對字串串聯符號("+"),即Java過載了+的功能。
  • Java語言提供了將任意型別物件轉換為字串的特殊支援(toString()方法)。

字串的特點

1、字串String型別本身是final宣告的,意味著我們不能繼承String。

2、String物件內部是用字元陣列進行儲存的

​ char[] value; 預設的長度 16
​ 擴容方式同 int newCapacity = (value.length << 1) + 2;

(1)JDK8的String內部是用字元陣列進行儲存的

private final char[] value;

"abc" 等效於 char[] data={ 'a' , 'b' , 'c' }

例如: 
String str = "abc";

相當於: 
char data[] = {'a', 'b', 'c'};     
String str = new String(data);
// String底層是靠字元陣列實現的。

(2)JDK9之後String內部是用byte陣列進行儲存的

private final byte[] value;
private final byte coder;//新增加的欄位,表示字串採用的編碼 1是UTF-16,0是LATIN1

定義的字串中,如果沒有漢字,每個字元將採用LATIN1編碼表儲存,如果字串中包含漢字等非ASCII碼錶的字元,儲存的編碼就是UTF-16,相比JDK1.8節約記憶體

3、final class String 不能有子類

4、實現了Comparable介面 字串物件是可以進行比較的

5、字串資料存在字串常量池內。。。如果存在相同的資料只會開闢一塊空間

字串的物件也是不可變物件,意味著一旦進行修改(不是重新賦值),就會產生新物件。

    @Test
    public void test01(){
        // 建立一個字串物件
        String originalString = "Hello";
        System.out.println("Original String: " + originalString);

        // 修改字串,實際上是建立了一個新的字串物件
        String newString = originalString.concat(" World");
        System.out.println("New String after concatenation: " + newString);

        // 檢視原始字串是否改變
        System.out.println("Original String after modification: " + originalString);
    }

構造字串物件

使用構造方法

  • public String() :初始化新建立的 String物件,以使其表示空字元序列。
  • String(String original): 初始化一個新建立的 String 物件,使其表示一個與引數相同的字元序列;換句話說,新建立的字串是該引數字串的副本。
  • public String(char[] value) :透過當前引數中的字元陣列來構造新的String。
  • public String(char[] value,int offset, int count) :透過字元陣列的一部分來構造新的String。
  • public String(byte[] bytes) :透過使用平臺的預設字符集解碼當前引數中的位元組陣列來構造新的String。
  • public String(byte[] bytes,String charsetName) :透過使用指定的字符集解碼當前引數中的位元組陣列來構造新的String。
//字串常量物件,推薦
String str = "hello";

// 無參構造,不推薦
String str1 = new String();

//建立"hello"字串常量的副本,不推薦
String str2 = new String("hello");

//透過字元陣列構造
char chars[] = {'a', 'b', 'c','d','e'};     
String str3 = new String(chars);
String str4 = new String(chars,0,3);

// 透過位元組陣列構造
byte bytes[] = {97, 98, 99 };     
String str5 = new String(bytes);
String str6 = new String(bytes,"GBK");

使用"+"

任意資料型別與"字串"進行拼接,結果都是字串

	public static void main(String[] args) {
		int num = 123456;
		String s = num + "";
		System.out.println(s);
		
		Student stu = new Student();
		String s2 = stu + "";//自動呼叫物件的toString(),然後與""進行拼接
		System.out.println(s2);
	}

 	字串常量+字串常量:  字串常量

    字串變數+字串變數:
    字串常量+字串變數:  new StringBuilder()
    字串變數+字串常量:

使用字面量的方式

str1 = 'Hello, World!'  
str2 = "Hello, World!"  
str3 = '''This is a  

可以使用方法

words = ['Hello', 'World']  
combined_str = ' '.join(words)  # 'Hello World'  

字串的常用方法

基礎操作

(1)boolean isEmpty():字串是否為空

(2)int length():返回字串的長度

(3)String concat(xx):拼接,等價於+

(4)boolean equals(Object obj):比較字串是否相等,區分大小寫

(5)boolean equalsIgnoreCase(Object obj):比較字串是否相等,不區分大小寫

(6)int compareTo(String other):比較字串大小,區分大小寫,按照Unicode編碼值比較大小

(7)int compareToIgnoreCase(String other):比較字串大小,不區分大小寫

(8)String toLowerCase():將字串中大寫字母轉為小寫

(9)String toUpperCase():將字串中小寫字母轉為大寫

(10)String trim():去掉字串前後空白符

查詢

(11)boolean contains(xx):是否包含xx

(12)int indexOf(xx):從前往後找當前字串中xx,即如果有返回第一次出現的下標,要是沒有返回-1

(13)int lastIndexOf(xx):從後往前找當前字串中xx,即如果有返回最後一次出現的下標,要是沒有返回-1

@Test
	public void test01(){
		String str = "尚矽谷是一家靠譜的培訓機構,尚矽谷可以說是IT培訓的小清華,JavaEE是尚矽谷的當家學科,尚矽谷的大資料培訓是行業獨角獸。尚矽谷的前端和UI專業一樣獨領風騷。";
		System.out.println("是否包含清華:" + str.contains("清華")); //true
		System.out.println("培訓出現的第一次下標:" + str.indexOf("培訓"));  //9
		System.out.println("培訓出現的最後一次下標:" + str.lastIndexOf("培訓"));
}

字串擷取

(14)String substring(int beginIndex) :返回一個新的字串,它是此字串的從beginIndex開始擷取到最後的一個子字串。

(15)String substring(int beginIndex, int endIndex) :返回一個新字串,它是此字串從beginIndex開始擷取到endIndex(不包含)的一個子字串。

@Test
	public void test01(){
		String str = "helloworldjavaatguigu";
		String sub1 = str.substring(5);
		String sub2 = str.substring(5,10);
		System.out.println(sub1);
		System.out.println(sub2);
	}

@Test
	public void test02(){
		String fileName = "快速學習Java的秘訣.dat";
		//擷取檔名
		System.out.println("檔名:" + fileName.substring(0,fileName.lastIndexOf(".")));
		//擷取字尾名
		System.out.println("字尾名:" + fileName.substring(fileName.lastIndexOf(".")));
	}

和字元相關

(16)char charAt(index):返回[index]位置的字元

(17)char[] toCharArray(): 將此字串轉換為一個新的字元陣列返回

(18)String(char[] value):返回指定陣列中表示該字元序列的 String。

(19)String(char[] value, int offset, int count):返回指定陣列中表示該字元序列的 String。

(20)static String copyValueOf(char[] data): 返回指定陣列中表示該字元序列的 String

(21)static String copyValueOf(char[] data, int offset, int count):返回指定陣列中表示該字元序列的 String

(22)static String valueOf(char[] data, int offset, int count) : 返回指定陣列中表示該字元序列的 String

(23)static String valueOf(char[] data) :返回指定陣列中表示該字元序列的 String

@Test
	public void test01(){
		//將字串中的字元按照大小順序排列
		String str = "helloworldjavaatguigu";
		char[] array = str.toCharArray();
		Arrays.sort(array);
		str = new String(array);
		System.out.println(str);
	}
	
	@Test
	public void test02(){
		//將首字母轉為大寫
		String str = "jack";
		str = Character.toUpperCase(str.charAt(0))+str.substring(1);
		System.out.println(str);
	}

編碼與解碼

(24)byte[] getBytes():編碼,把字串變為位元組陣列,按照平臺預設的字元編碼進行編碼

​ byte[] getBytes(字元編碼方式):按照指定的編碼方式進行編碼

(25)new String(byte[] ) 或 new String(byte[], int, int):解碼,按照平臺預設的字元編碼進行解碼

​ new String(byte[],字元編碼方式 ) 或 new String(byte[], int, int,字元編碼方式):解碼,按照指定的編碼方式進行解碼

@Test
	public void test01(){
		//將字串中的字元按照大小順序排列
		String str = "helloworldjavaatguigu";
		char[] array = str.toCharArray();
		Arrays.sort(array);
		str = new String(array);
		System.out.println(str);
	}
	
	@Test
	public void test02(){
		//將首字母轉為大寫
		String str = "jack";
		str = Character.toUpperCase(str.charAt(0))+str.substring(1);
		System.out.println(str);
	}

開頭與結尾

(26)boolean startsWith(xx):是否以xx開頭

(27)boolean endsWith(xx):是否以xx結尾

	@Test
	public void test2(){
		String name = "張三";
		System.out.println(name.startsWith("張"));
	}
	
	@Test
	public void test(){
		String file = "Hello.txt";
		if(file.endsWith(".java")){
			System.out.println("Java原始檔");
		}else if(file.endsWith(".class")){
			System.out.println("Java位元組碼檔案");
		}else{
			System.out.println("其他檔案");
		}
	}

替換

(29)String replace(xx,xx):不支援正則

(30)String replaceFirst(正則,value):替換第一個匹配部分

(31)String repalceAll(正則, value):替換所有匹配部分

	@Test
	public void test4(){
		String str = "hello244world.java;887";
		//把其中的非字母去掉
		str = str.replaceAll("[\\d]", "");
		System.out.println(str);
	}

拆分(分割)

(32)String[] split(正則):按照某種規則進行拆分

	@Test
	public void test4(){
		String str = "張三.23|李四.24|王五.25";
		//|在正則中是有特殊意義,我這裡要把它當做普通的|
		String[] all = str.split("\\|");
		
		//轉成一個一個學生物件
		Student[] students = new Student[all.length];
		for (int i = 0; i < students.length; i++) {
			//.在正則中是特殊意義,我這裡想要表示普通的.
			String[] strings = all[i].split("\\.");//張三,  23
			String name = strings[0];
			int age = Integer.parseInt(strings[1]);
			students[i] = new Student(name,age);
		}
		
		for (int i = 0; i < students.length; i++) {
			System.out.println(students[i]);
		}
		
	}
	
	@Test
	public void test3(){
		String str = "1Hello2World3java4atguigu5";
		str = str.replaceAll("^\\d|\\d$", "");
		String[] all = str.split("\\d");
		for (int i = 0; i < all.length; i++) {
			System.out.println(all[i]);
		}
	}
	
	@Test
	public void test2(){
		String str = "1Hello2World3java4atguigu";
		str = str.replaceFirst("\\d", "");
		System.out.println(str);
		String[] all = str.split("\\d");
		for (int i = 0; i < all.length; i++) {
			System.out.println(all[i]);
		}
	}
	
	
	@Test
	public void test1(){
		String str = "Hello World java atguigu";
		String[] all = str.split(" ");
		for (int i = 0; i < all.length; i++) {
			System.out.println(all[i]);
		}
	}

字串常量池

字串常量物件可以共享的原因和好處

字串常量物件可以共享的原因:字串物件不可變

字串常量物件共享的好處:節省記憶體

String s1 = "atguigu";
String s2 = "atguigu";
System.out.println(s1 == s2);//這裡只建立了一個字串物件"atguigu"。

s2 = s2.replace("a","o");
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);

這裡s1指向的"atguigu"和s2指向的"atguigu"是同一個。
如果無法保證"atguigu"物件不可變,那麼當s2將"a"替換為"o"之後,那麼s1就會受到影響,這樣是不安全的。
但是,現在我們發現s1並未受到影響,也就是說,s1指向的"atguigu"物件並未被修改,而是基於"atguigu"重新複製了一個新物件"atguigu",然後替換成"otguigu"。

hashCode方法

Object類有一個int hashCode()方法,該方法用於計算物件的雜湊值。雜湊值的作用就好比生活中的身份證號,用一串數字代表一個物件。雜湊值的計算是有講究的,按照常規協定hashCode方法和equals方法要一起重寫,要求兩個“相等”的物件hashCode必須相同,如果兩個物件的雜湊值不同,它倆呼叫equals方法也必須是false,但是如果兩個物件的雜湊值相同,它倆呼叫equals方法卻不一定true。

    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;
    }
在計算雜湊碼時選擇31作為乘數,主要是基於以下幾個原因:
1. 質數特性
減少雜湊衝突:31是一個質數(素數),在雜湊計算中選擇質數作為乘數可以減少雜湊衝突的可能性。質數在乘法運算中產生的結果更分散,使得不同的輸入值更有可能產生不同的雜湊碼,從而減少相同值產生相同雜湊碼的機率。
2. 合適的範圍
避免溢位和過小範圍:如果選擇一個較小的質數(如2),那麼得出的乘積會在一個很小的範圍,很容易造成雜湊值的衝突。而如果選擇一個100以上的質數,得出的雜湊值可能會超出int的最大範圍(在Java等語言中),導致溢位。31作為一個“不大不小”的質數,能夠較好地平衡這兩個問題。
3. JVM最佳化
位運算最佳化:JVM裡最有效的計算方式就是進行位運算。31乘以一個整數i可以最佳化為(i << 5) - i,即先將i左移5位(相當於乘以32),然後再減去i本身。這種最佳化減少了乘法運算,提高了計算效率。
4. 實驗驗證
低衝突率:透過大量實驗驗證,使用31作為乘數,在處理大量資料時(如超過50,000個英文單詞),雜湊值的衝突數相對較低。這進一步證明了31作為乘數的有效性。

字串常量池

字串常量池是一個雜湊表,它裡面記錄了可以共享使用的字串常量物件的地址。採用雜湊表結構的目的是為了提高效能,用空間換時間。字串物件的地址雜湊儲存在雜湊表中,雖然是雜湊儲存,但是因為可以使用字串物件的hashCode值快速的計算儲存位置的下標,所以效率還是很高的。

image-20240718113615995

String s1 = "hello";
String s2 = "hello";

當給s1賦值"hello"時,根據"hello"的hashCode()值,計算出來index=[2],如果table[2]=null,那就把"hello"物件的字串的地址0x7534放到table[2]中。
    
當給s2賦值"hello"時,根據"hello"的hashCode()值,計算出來index=[2],此時table[2]!=null,那就直接把"hello"的記憶體地址0x7534賦值給s2。
    
String s3 = "Aa";
String s4 = "BB";
當給s3賦值"Aa"時,根據"Aa"的hashCode()值,計算出來index=[6],如果table[6]=null,那就把"Aa"物件的字串的地址0x8989放到table[6]中。
    
當給s4賦值"BB"時,根據"BB"的hashCode()值,計算出來index=[6],此時table[index]=table[6]!=null,但是"BB"和"Aa"不一樣,那就直接把"BB"的記憶體地址0x6666也放到table[6]中,相當於table[6]中記錄了兩個字串物件的地址,它們使用連結串列連線起來。    

不同方法建立字串的儲存位置

直接賦值字串:儲存到字串常量池中

//s指向常量池中的引用
String s ="lizhi";  //只會儲存到字串常量池中
//建立字串時jvm會判斷字串常量池中是否有“該字串”如果有直接返回物件的引用。否則會建立一個新的字串常量

image-20240801100828288

使用new 關鍵字的字串

//str指向記憶體中的物件引用
String str =new String("lizhi");
/*
透過new關鍵字建立的字串引用,字串常量池和堆記憶體都會有這個物件,沒有就建立,最後返回的是堆記憶體中的物件引用(堆中地址)。
因為有1izhi這個字面量,先去檢查《字串常量池中》是否存在該字串
如果不存在,就直接先在字串常量池中建立一個字串物件,然後再去堆記憶體中建立一個字串物件1izi;
如果存在,就直接去堆記憶體建立一個字串物件(在堆中開闢空間),內容為1izhi(指向常量池中物件);
最後將堆記憶體的字串引用返回(堆中地址)。
*/

//<<這種方式會存在兩個物件>>

image-20240801100718454

使用"+"連線字串

//s指向常量池中的引用
String s="a"+"b"+"c"; //"abc"

字串常量+字串常量:  字串常量
    
字串變數+字串變數:
字串常量+字串變數:  new StringBuilder()
字串變數+字串常量:

字串的intern()方法:

  • 當呼叫 intern 方法時,首先會在字串常量池中判斷是否有該物件引用,如果有直接** **
  • 否則,將此 String 物件新增到字串常量池中,並返回此 String 物件的引用
  • 注意:新增到字串常量池中,是指把堆中物件的引用新增到常量池中。
String s1 = new String("lizhi");   
s1.intern();
String s3 = "s";
System.out.println(s3 == s1); //false

image-20240801100944322

String s1 = new String("li");   
String s2 = s1 +"zhi"
String s3 = s2.intern();
System.out.println(s2 == s3); //true

image-20240801102635985

String s1 = "s".intern(); //首先將s新增到字串常量池中,並返回引用
String s="s";
System.out.println(s==s1);  //true 

String s1 = new String("s")+new String("b"); //
s1.intern();
String s3 = "sb";
System.out.println(s3==s1); //true 

public class TestStringIntern {
    @Test
    public void test1(){
        String s1 = new String("hello");
        String s2 = s1.intern();
        System.out.println(s1 == s2);
        /*
        JDK8:false
         */
    }

    @Test
    public void test2(){
        String s1 = "he".concat("llo");
        String s2 = s1.intern();
        System.out.println(s1 == s2);
        /*
        JDK8:true
         */
    }
}

哪些字串物件地址放入字串常量池?

需要共享的字串地址記錄到字串常量池的table表中

需要共享的字串地址記錄到字串常量池的table表中,不需要共享的字串物件其地址值不需要記錄到字串常量池的table表中。除了以下2種,其他的都不放入字串常量池:
(1)""直接的字串 (備註:兩個""的字串直接+,編譯器處理成一個""字串)
(2)字串物件.intern()結果
 
其他:
(1)直接new
(2)valueOf,copyValueOf等
(3)字串物件拼接:concat拼接 以及 字串變數 + 拼接
(4)toUpperCase,toLowerCase,substring,repalce等各種String方法得到的字串
這些方式,本質都是新new的。

字串物件和字串常量池在哪裡?

字串常量池表:

  • JDK1.6:在方法區的永久代
  • JDK1.7之後:

字串物件:

  • JDK1.7之前:需要共享的字串物件儲存在方法區的永久代,然後把物件地址記錄到字串常量池的table表中,不需要共享的字串物件儲存在堆中,其地址值不需要記錄到字串常量池的table表中。
  • JDK1.7之後:所有字串物件都儲存在堆中。同樣需要共享的字串地址記錄到字串常量池的table表中,不需要共享的字串物件其地址值不需要記錄到字串常量池的table表中。

StringBuilder&StringBuffer

因為String物件是不可變物件,雖然可以共享常量物件,但是對於頻繁字串的修改和拼接操作,效率極低。因此,JDK又在java.lang包提供了可變字元序列StringBuilder和StringBuffer型別。

StringBuilder,StringBuffer區別

StringBuffer:老的,執行緒安全的(因為它的方法有synchronized修飾),效率低

StringBuilder:執行緒不安全的,效率高

相同點: 可變的字串物件 char[] value; 預設的長度 16

擴容方式同 int newCapacity = (value.length << 1) + 2;

常用API

常用的API,StringBuilder、StringBuffer的API是完全一致的

(1)StringBuffer append(xx):拼接,追加

(2)StringBuffer insert(int index, xx):在[index]位置插入xx

(3)StringBuffer delete(int start, int end):刪除[start,end)之間字元

​ StringBuffer deleteCharAt(int index):刪除[index]位置字元

(4)void setCharAt(int index, 值):替換[index]位置字元值;

(5)StringBuffer reverse():反轉

(6)void setLength(int newLength) :設定當前字元序列長度為newLength

(7)StringBuffer replace(int start, int end, String str):替換[start,end)範圍的字元序列為str

(8)int indexOf(String str):在當前字元序列中查詢str的第一次出現下標

​ int indexOf(String str, int fromIndex):在當前字元序列[fromIndex,最後]中查詢str的第一次出現下標

​ int lastIndexOf(String str):在當前字元序列中查詢str的最後一次出現下標

​ int lastIndexOf(String str, int fromIndex):在當前字元序列[fromIndex,最後]中查詢str的最後一次出現下標

(9)String substring(int start):擷取當前字元序列[start,最後]

(10)String substring(int start, int end):擷取當前字元序列[start,end)

(11)String toString():返回此序列中資料的字串表示形式

String 和 StringBuffer的區別

​ 1.可變與不可變
​ String
​ StringBuffer
​ 2.記憶體區域
​ String 堆中 堆中的常量池
​ StringBffer 堆中
​ 3.拼接效率
​ String 低
​ StringBuffer 高

相關文章