一文看完String的前世今生,内容有点多,请耐心看完!
寫在開頭
String字符串作為一種引用類型,在Java中的地位舉足輕重,也是代碼中出現頻率最高的一種數據結構,因此,我們需要像分析Object一樣,將String作為一個topic,單獨拿出來總結,這里面涉及到字符串的不可變性,字符串拼接、存儲、比較、截取以及StringBuffer,StringBuilder區別等。
String類的源碼
源碼解讀
想要真切的去了解Java中被定義好的一個類,讀源碼是最直接的方式,以經典的Java8為例(Java9之后,內部的實現數組類型從char改為了byte,目的用來節省內存空間),我們來看看Java中對于String是如何設計的。
public final class String implements java.io.Serializable, 
Comparable<String>, CharSequence {
    private final char value[];
  //...
}
我們從源碼中可以總結出如下幾點內容:
1. String類被final關鍵字修飾,意味著它不可被繼承;;
 2. String的底層采用了final修飾的char數組,意味著它的不可變性;
 3. 實現了Serializable接口意味著它支持序列化;
 4. 實現了Comparable接口,意味著字符串的比較可以采用compareTo()方法,而不是==號,并且Sring類內部也重寫了Object的equals()方法用來比較字符串相等。
String如何實現不可變得性?
從過源碼我們可以看到類和char[]數組均被final關鍵字修飾,且數組的訪問修飾符為private,訪問權限僅限本類中。
final關鍵字修飾的類不能被繼承,修飾的方法不能被重寫,修飾的變量是基本數據類型則值不能改變,修飾的變量是引用類型則不能再指向其他對象。但光用final修飾只能保證不被子類繼承,不存在子類的破壞,char數組中的字符串仍然是可以改變的。
但,當底層實現的這個char[]被private修飾后,代表著它的私有化,且String沒有對外提供修改這個字符串數組的方法,這才導致了它的不可變!
String如為什么要不可變?
那么問題來了,String為什么要設計成不可變的呢?我們都知道,不可變意味著,每次賦值其實就是創建一個新的字符串對象進行存儲,這無疑帶來了諸多不便。但相比于以下2點,那些不便似乎無關緊要了
1、String 類是最常用的類之一,為了效率,禁止被繼承和重寫
2、為了安全。String 類中有很多調用底層的本地方法,調用了操作系統的API,
如果方法可以重寫,可能被植入惡意代碼,破壞程序。其實Java 的安全性在這里就有一定的體現啦。
String類的方法
因為使用頻率非常高,所以String內部提供很多操作字符串的方法,常用的如下:
equals:字符串是否相同
equalsIgnoreCase:忽略大小寫后字符串是否相同
compareTo:根據字符串中每個字符的Unicode編碼進行比較
compareToIgnoreCase:根據字符串中每個字符的Unicode編碼進行忽略大小寫比較
indexOf:目標字符或字符串在源字符串中位置下標
lastIndexOf:目標字符或字符串在源字符串中最后一次出現的位置下標
valueOf:其他類型轉字符串
charAt:獲取指定下標位置的字符
codePointAt:指定下標的字符的Unicode編碼
concat:追加字符串到當前字符串
isEmpty:字符串長度是否為0
contains:是否包含目標字符串
startsWith:是否以目標字符串開頭
endsWith:是否以目標字符串結束
format:格式化字符串
getBytes:獲取字符串的字節數組
getChars:獲取字符串的指定長度字符數組
toCharArray:獲取字符串的字符數組
join:以某字符串,連接某字符串數組
length:字符串字符數
matches:字符串是否匹配正則表達式
replace:字符串替換
replaceAll:帶正則字符串替換
replaceFirst:替換第一個出現的目標字符串
split:以某正則表達式分割字符串
substring:截取字符串
toLowerCase:字符串轉小寫
toUpperCase:字符串轉大寫
trim:去字符串首尾空格
方法有很多,無法一一講解,我們挑選幾個聊一聊哈
方法1、hashCode
由于Object中有hashCode()方法,所以所有的類中都有對應的方法,在String中做了如下的實現:
private int hash; // 緩存字符串的哈希碼
public int hashCode() {
    int h = hash; // 從緩存中獲取哈希碼
    // 如果哈希碼未被計算過(即為 0)且字符串不為空,則計算哈希碼
    if (h == 0 && value.length > 0) {
        char val[] = value; // 獲取字符串的字符數組
        // 遍歷字符串的每個字符來計算哈希碼
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i]; // 使用 31 作為乘法因子
        }
        hash = h; // 緩存計算后的哈希碼
    }
    return h; // 返回哈希碼
}
先檢核是否已計算哈希,若已計算則直接返回,否則根據31倍哈希法進行計算并緩存計算后的哈希值。String中重寫后的hashCode方法,計算效率高,隨機性強,哈希碰撞概率小,所以常被用作HashMap中的Key。
方法2、equals
我們在之前的文章中曾提到過重寫equals方法往往也需要重寫hashCode方法,這一點String就做到了,我們來看看String中equals()方法的實現:
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            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;
    }
這是Java8中的實現,邏輯清晰易懂,首先,通過==判斷是否是同一個對象,如果是則直接返回true,否則進入下一輪判斷邏輯:判斷對象是否為String類型,再判斷兩個字符串長度是否相等,再比較每個字符是否相等,全部為true最后返回true,其中有任何一個為flase則返回false。
方法3、substring
該方法在日常開發中時常被用到,主要用來截取字符串,源碼:
public String substring(int beginIndex) {
    // 檢查起始索引是否小于 0,如果是,則拋出 StringIndexOutOfBoundsException 異常
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    // 計算子字符串的長度
    int subLen = value.length - beginIndex;
    // 檢查子字符串長度是否為負數,如果是,則拋出 StringIndexOutOfBoundsException 異常
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    // 如果起始索引為 0,則返回原字符串;否則,創建并返回新的字符串
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
使用案例:
注意源碼中注釋提到的:如果 beginIndex 為 0,說明子串與原字符串相同,直接返回原字符串。否則,使用 value 數組(原字符串的字符數組)的一部分 new 一個新的 String 對象并返回。
String str = "Hello, world!";
String pre = str.substring(0);
System.out.println(pre);
String prefix = str.substring(0, 5);  
System.out.println(prefix);
String suffix = str.substring(7);     
System.out.println(suffix);
輸出:
Hello, world!
Hello
world!
方法4、indexOf
indexOf的主要作用是獲取目標字符或字符串在源字符串中位置下標,看源碼:
/**
     * 由 String 和 StringBuffer 共享的用于執行搜索的代碼。這
     * source 是正在搜索的字符數組,目標
     * 是要搜索的字符串。
     *
     * @param正在搜索的字符的來源。
     * @param源字符串的 sourceOffset 偏移量。
     * @param源字符串的 sourceCount 計數。
     * @param定位正在搜索的字符。
     * @param目標字符串的 targetOffset 偏移量。
     * @param目標字符串的 targetCount 計數。
     * @param fromIndex 要開始搜索的索引。
     */
static int indexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) {
        if (fromIndex >= sourceCount) {
            return (targetCount == 0 ? sourceCount : -1);
        }
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (targetCount == 0) {
            return fromIndex;
        }
        char first = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);
        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            /* Look for first character. */
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }
            /* Found first character, now look at the rest of v2 */
            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);
                if (j == end) {
                    /* Found whole string. */
                    return i - sourceOffset;
                }
            }
        }
        return -1;
    }
使用案例一
String str = "Hello, world!";
int index = str.indexOf("wor");  // 查找 "world" 子字符串在 str 中第一次出現的位置
System.out.println(index);        // 輸出 7,字符串下標從0開始,空格也算一位
使用案例二
String str = "Hello, world!";
int index1 = str.indexOf("o");    // 查找 "o" 子字符串在 str 中第一次出現的位置
int index2 = str.indexOf("o", 5); // 從索引為5的位置開始查找 "o" 子字符串在 str 中第一次出現的位置
System.out.println(index1);       // 輸出 4
System.out.println(index2);       // 輸出 8
方法五、replace與replaceAll
話不多說,直接看碼
///replace是字符和字符串的替換操作,基于字符匹配
public String replace(char oldChar, char newChar)
public String replace(CharSequence target, CharSequence replacement)
//replaceAll是基于正則表達式的字符串匹配與替換
public String replaceAll(String regex, String replacement)
使用案例:
String str = "Hello Java. Java is a language.";
//查找原字符串中所有Java子串,并用c進行替換
System.out.println(str.replace("Java", "c"));
//根據正則表達式匹配規則,.代表是任意字符 可以匹配任何單個字符
//所以經過正則匹配后,找出原字符串中所有“Java”+”任意一個字符”的子串,用c進行替換!
System.out.println(str.replaceAll("Java.", "c"));
輸出:
Hello c. c is a language.
Hello c cis a language.
String類的使用
學以致用,學習的最終目的就是使用!
字符串常量池
搞清楚字符串常量池之前,我們先看如下這條語句,考你們一下,這行代碼創建了幾個對象?
String s1 = new String("abc");
這個答案并不是唯一的
第一種情況:若字符串常量池中不存在“abd”對象的引用,則語句會在堆中闖將2個對象,其中一個對象的引用保存到字符串常量池中。
這種情況下的字節碼(JDK1.8)
ldc 命令用于判斷字符串常量池中是否保存了對應的字符串對象的引用,如果保存了的話直接返回,如果沒有保存的話,會在堆中創建對應的字符串對象并將該字符串對象的引用保存到字符串常量池中。	
第二種情況: 如果字符串常量池中已存在字符串對象“abc”的引用,則只會在堆中創建 1 個字符串對象“abc”。
// 字符串常量池中已存在字符串對象“abc”的引用
String s1 = "abc";
// 下面這段代碼只會在堆中創建 1 個字符串對象“abc”
String s2 = new String("abc");
看到這里我們大致可以明白什么時字符串常量池,以及它的作用了:
字符串常量池是JVM 為了提升性能和減少內存消耗針對字符串(String 類)專門開辟的一塊區域,主要目的是為了避免字符串的重復創建。
字符串引用的存儲
在上面的內容中,我們了解了字符串常量池,那么Java中是怎么將字符串的引用保存到常量池中的呢,這里我們需要提到String的intern()方法。
String.intern() 是一個native(本地)方法
其作用是將指定的字符串對象的引用保存在字符串常量池中。
若字符串常量池中保存了對應的字符串對象的引用,就直接返回該引用;
若字符串常量池中沒有保存了對應的字符串對象的引用,那就在常量池中創建一個指向該字符串對象的引用并返回。
我們看下面一段代碼:
String s1 = new String("Hello") + new String("World");
String s2 = s1.intern();
System.out.println(s1 == s2);
你們覺得返回的是false還是true?如果還不明白,那么請看一下美團團隊發布的一篇文章
美團技術團隊深入解析 String.intern()
字符串的拼接
你是不是曾用過“+”進行字符串的拼接操作,比如說String res = "str" + "ing"; 。,最終輸出的就是string
出現這樣的效果的原因是Java編譯器的優化功能-常量折疊!
對于編譯期可以確定值的字符串,也就是常量字符串 ,jvm 會將其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在編譯階段就已經被存放字符串常量池,這個得益于編譯器的優化。
優化前:String res = "str" + "ing";
優化后:String res = "string";
但像對象引用這種情況,無法在編譯其進行優化,我們看下面這段
String str1 = "str";
String str2 = "ing";
System.out.println(str1+str2);
字節碼(JDK1.8)
通過字節碼我們可以分析出,通過+號將幾個對象引用進行拼接,實際上是調用StringBuilder().append(str1).append(str2).toString();來實現的。
但有幾個對象引用拼接,就會創建幾個StringBuilder對象,浪費資源,因此,在做字符串拼接時直接采用StringBuilder實現!
String、StringBuffer,StringBuilder區別
相同點:
1、都可以儲存和操作字符串
2、都使用 final 修飾,不能被繼承
3、提供的 API 相似
異同點:
1、String 是只讀字符串,String 對象內容是不能被改變的
2、StringBuffer 和 StringBuilder 的字符串對象可以對字符串內容進行修改,在修改后的內存地址不會發生改變
3、StringBuilder 線程不安全;StringBuffer 線程安全
三者區別詳解請點擊看這篇文章
總結
以上是生活随笔為你收集整理的一文看完String的前世今生,内容有点多,请耐心看完!的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 反沙箱技术
 - 下一篇: Spring AOP原来是这样实现的