Java String的相關性質分析
阿新 • • 發佈:2020-07-01
## 引言
String可以說是在Java開發中必不可缺的一種類,String容易忽略的細節也很多,對String的瞭解程度也反映了一個Java程式設計師的基本功。下面就由一個面試題來引出對String的剖析。
## **1. String在原始碼裡究竟是如何實現的,它有哪些方法,有什麼作用?**
從原始碼可以看出,String有三個私有方法,底層是由字元陣列來儲存字串
```java
public final class String
implements java.io.Serializable, Comparable, CharSequence {
/**儲存字串的字元陣列*/
private final char value[];
/** 快取字串的hashcode */
private int hash; // 預設是0
/** 用於驗證一致性來是否進行反序列化 */
private static final long serialVersionUID = -6849794470754667710L;
```
### 1.1 String重要構造方法
```java
// String 為引數的構造方法
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// char[] 為引數構造方法
public String(char value[]) {
//重新複製一份char陣列的值和資訊,保證字串不會被修改傳回
this.value = Arrays.copyOf(value, value.length);
}
// StringBuffer 為引數的構造方法
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
// StringBuilder 為引數的構造方法
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
```
### 1.2 String重要的方法
#### **1.2.1 equals()方法**
```java
/**比較兩個字串是否相等,返回值為布林型別*/
public boolean equals(Object anObject) {//比較型別可以是object
/*引用物件相同時返回true*/
if (this == anObject) {
return true;
}
/*判斷引用物件是否為String型別*/
if (anObject instanceof String) { //instanceof用來判斷資料型別是否一致
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
//將兩個比較的字串轉換成字元陣列
char v1[] = value;
char v2[] = anotherString.value;
//一個一個字元進行比較
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
```
`equals()`方法首先通過instanceof判斷資料型別是否一致,是則進行下一步將兩個字串轉換成字元陣列逐一判斷。最後再返回判斷結果。
#### 1.2.2 compareTo()方法
```java
/*比較兩個字串是否相等,返回值為int型別*/
public int compareTo(String anotherString) {//比較型別只能是String型別
int len1 = value.length;
int len2 = anotherString.value.length;
/*獲得兩字串最短的字串長度lim*/
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
/*逐一比較兩字元組的字元*/
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
//若兩字元不相等,返回c1-c2
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
```
`compareTo()`通過逐一判斷兩字串中的字元,不相等則返回兩字元差,反之迴圈結束最後返回0
##### 小結
1. `compareTo()`和`equals()`都能比較兩字串,當equals()返回true,compareTo()返回0時,都表示兩字串完全相同。
2. 同時兩者也有區別:
* 返回型別`compareTo()`是boolean,`equals()`是int。
* 字元型別`compareTo()`是Object,`equals()`只能是String型別。
### 1.3其他方法
1. `indexOf()`:查詢字串首次出現的下標位置
2. `lastIndexOf()`:查詢字串最後出現的下標位置
3. `contains()`:查詢字串中是否包含另一個字串
4. `toLowerCase()`:把字串全部轉換成小寫
5. `toUpperCase()`:把字串全部轉換成大寫
6. `length()`:查詢字串的長度
7. `trim()`:去掉字串首尾空格
8. `replace()`:替換字串中的某些字元
9. `split()`:把字串分割並返回字串陣列
10. `join()`:把字串陣列轉為字串
知道了String的實現和方法,下面就要引出常見的String面試問題
## 2. String常見的面試問題
### 2.1 為什麼String型別要用final修飾?
* 從上面的程式碼可以看出,String類是被private final修飾的不可繼承類。那麼為何要用final修飾呢?
> Java 語言之父 James Gosling 的回答是,他會更傾向於使用 final,因為它能夠快取結果,當你在傳參時不需要考慮誰會修改它的值;如果是可變類的話,則有可能需要重新拷貝出來一個新值進行傳參,這樣在效能上就會有一定的損失。
>
>James Gosling 還說迫使 String 類設計成不可變的另一個原因是安全,當你在呼叫其他方法時,比如呼叫一些系統級操作指令之前,可能會有一系列校驗,如果是可變類的話,可能在你校驗過後,它的內部的值又被改變了,這樣有可能會引起嚴重的系統崩潰問題,這是迫使 String 類設計成不可變類的一個重要原因。
所以只有當字串不可改變時,才能利用字元常量池,保證在使用字元的時候不會被修改。
* 那麼問題來了,我們在使用final修飾一個變數時,不變的是引用地址,引用地址對應的物件是可以發生變化的。如:
```java
import java.util.Arrays;
public class IntTest{
public static void main(String args[]){
final char[] arr = new char[]{'a', 'b', 'c', 'd'};
System.out.println("arr的地址1:" + arr);
System.out.println("arr的值2:" + Arrays.toString(arr));
arr[2] = 'b';//修改arr[2]的值
/**修改arr陣列的地址,這裡會發生編譯錯誤,所以無法修改引用地址
arr = new char[]{'1', '2', '3'};*/
System.out.println("arr的地址2:" + arr);
System.out.println("arr的值2:" + Arrays.toString(arr));
}
}
/*執行結果:
arr的地址1:[C@15db9742
arr的值1:[a b c d]
arr的地址2:[C@15db9742
arr的值2:[a b b d]
顯然不變的是引用地址,引用地址所指物件的內容可以被修改
*/
```
而在上面1中的原始碼裡,String類下有一個私有的char陣列成員
```java
public final class String
implements java.io.Serializable, Comparable, CharSequence {
/**儲存字串的字元陣列*/
private final char value[];
```
那麼是否可以通過修改char陣列所指物件的內容,來改變string的值呢?來試一試:
```java
import java.util.Arrays;
public class IntTest{
public static void main(String args[]){
char[] arr = new char[]{'a','b','c','d'};
String str = new String(arr);
System.out.println("arr的地址1:" + arr);
System.out.println("str= " + str);
System.out.println("arr[]= "+Arrays.toString(arr));
arr[2]='b';//修改arr[2]的值
System.out.println("arr的地址2:" + arr);
System.out.println("str= "+str);
System.out.println("arr[]= "+Arrays.toString(arr));
}
}
/*執行結果:
arr的地址1:[C@15db9742
str= abcd
arr[]= [a, b, c, d]
arr的地址2:[C@15db9742
str= abcd
arr[]= [a, b, b, d]
*/
```
顯然無法修改字串,這是為何,我們再看看構造方法
```java
// String 為引數的構造方法
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// char[] 為引數構造方法
public String(char value[]) {
//重新複製一份char陣列的值和資訊,保證字串不會被修改傳回
this.value = Arrays.copyOf(value, value.length);
}
```
發現string的構造方法裡將原來的char陣列的值和資訊copy了一份,保證字串不會被修改傳回。
### 2.2 equals()和 == 的區別
#### 2.2.1 先說結論:
* ==在基本型別中比較其對應的值,在引用型別中比較其地址值
* equals()在未被重寫時和 == 完全一致,被重寫後是比較字串的值
```java
public class StringTest {
public static void main(String args[]) {
String str1 = "Java"; //放在常量池中
String str2 = new String("Java"); //在堆中建立物件str2的引用
String str3 = str2; //指向堆中的str2的物件的引用
String str4 = "Java"; //從常量池中查詢
String str5 = new String("Java");
System.out.println(str1 == str2); //false
System.out.println(str1 == str3); //false
System.out.println(str1 == str4); //true
System.out.println(str2 == str3); //true
System.out.println(str2 == str5); //false
System.out.println(str1.equals(str2)); //true
System.out.println(str1.equals(str3)); //true
System.out.println(str1.equals(str4)); //true
System.out.println(str2.equals(str3)); //true
}
}
```
實際上`equals()`方法也是繼承Object的`equals()`方法。
```java
public boolean equals(Object obj) {
return (this == obj);
}
```
從上面的`equals()`方法的原始碼可以看出,String在繼承方法後對應修改了方法中的相關內容,所以上述程式碼的`equals()`方法輸出都是true。
類似於`String str1 = "Java"; ` 的和`String str2 = new String("Java");`形式有很大的區別,**`String str1 = "Java"; `形式首先在編譯過程中Java虛擬機器就會去常量池中查詢是否存在“Java”,如果存在,就會在棧記憶體中開闢一塊地方用於儲存其常量池中的地址。所以這種形式有可能建立了一個物件(常量池中),也可能一個物件也沒建立**,即str1是直接在常量池中建立“Java”字串,str4是先在常量池中查詢有“Java”,所以直接地址直接指向常量池中已經存在的”Java“字串。
**而`String str2 = new String("Java");`的形式在編譯過程中,先去常量池中查詢是否有“Java”,沒有則在常量池中新建"Java"。到了執行期,不管常量池中是否有“Java”,一律重新在堆中建立一個新的物件,然如果常量池中存在“Java”,複製一份放在堆中新開闢的空間中。如果不存在則會在常量池中建立一個“Java”後再複製到堆中。所以這種形式至少建立了一個物件,最多兩個物件。**因此str1和str2的引用地址必然不相同。
![](https://img2020.cnblogs.com/blog/1707576/202007/1707576-20200701172236572-1898248148.png)
### 2.3 string中的intern()方法
呼叫intern方法時,如果常量池中存在該字串,則返回池中的字串。否則將此字串物件新增到常量池中,並返回該字串的引用。
```java
String s1 = new String("Java");
String s2 = s1.intern();//直接指向常量池中的字串
String s3 = "Java";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true
```
![](https://img2020.cnblogs.com/blog/1707576/202007/1707576-20200701172223495-261992092.png)
### 2.4 String和StringBuilder、StringBuffer的區別
關於這三者的區別,主要借鑑這篇博文[String,StringBuffer與StringBuilder的區別??](https://blog.csdn.net/rmn190/article/details/1492013)首先,**String是字串常量,後兩者是字串變數。其中StringBuffer是執行緒安全的**,下面說說他們的具體區別。
String適用於字串不可變的情況,因為在經常改變字串的情形下,每次改變都會在堆記憶體中新建物件,會造成 JVM GC的工作負擔,因此在這種情形下,需要使用字串變數。
再說StringBuffer,它是執行緒安全的可變字元序列,它提供了append和insert方法用於字串拼接,並用synchronized來保證執行緒安全。並且可以對這些方法進行同步,像以序列順序發生,而且該順序與所涉及的每個執行緒進行的方法呼叫順序一致。
```java
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
```
最後是StringBuilder,因為StringBuffer要保證執行緒安全,所以效能不是很高,於是在JDK1.5後引入了StringBuilder,在沒有了synchronize後效能得到提高,而且兩者的方法基本相同。所以在非併發操作下,如單執行緒情況可以使用StringBuilder來對字串進行修改。
### 2.5 String中的“ + ”操作符
其實在2.4中提到,String是字串常量,具有不可變性。所以在拼接字串、修改字串時,儘量選擇StringBuilder和StringBuffer。下面再談一談String中出現“+”操作符的情況:
```java
String s1 = "Ja";
String s2 = "va";
String s3 = "Java";
String s4 = "Ja" + "va"; //在編譯時期就在常量池中建立
String s5 = s1 + s2; //實際上s5是stringBuider,這個過程是stringBuilder的append
System.out.println("s3 == s4 " + (s3 == s4));
System.out.println("s3 == s5 " + (s3 == s5));
/**
執行結果:
s3 == s4 true
s3 == s5 false
*/
```
為什麼s4==s3結果是true? 反編譯看看:
```java
1 String s = "Ja";//s1
2 String s1 = "va";//s2
3 String s2 = "Java";//s3
4 String s3 = "Java";//s4
5 String s4 = (new StringBuilder()).append(s).append(s1).toString();//s5
6 System.out.println((new StringBuilder()).append("s3 == s4").append(s2 == s3).toString());
7 System.out.println((new StringBuilder()).append("s3 == s5").append(s2 == s4).toString());
```
從第5行程式碼中看出s4在編譯時期就已經將“Ja”+“va”編譯“Java” ,這就是JVM的優化
第6行的程式碼說明在`s5 = s1 +s2;`執行過程,s5變成StringBuilder,並利用append方法將s1和s2拼接。
因此在String型別中使用“+”操作符,編譯器一般會將其轉換成new StringBuilder().append()來