程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 計算機程序的思維邏輯 (29),思維29

計算機程序的思維邏輯 (29),思維29

編輯:JAVA綜合教程

計算機程序的思維邏輯 (29),思維29


上節介紹了單個字符的封裝類Character,本節介紹字符串類。字符串操作大概是計算機程序中最常見的操作了,Java中表示字符串的類是String,本節就來詳細介紹String。

字符串的基本使用是比較簡單直接的,我們來看下。

基本用法

可以通過常量定義String變量

String name = "老馬說編程";

也可以通過new創建String

String name = new String("老馬說編程");

String可以直接使用+和+=運算符,如:

String name = "老馬";
name+= "說編程";
String descritpion = ",探索編程本質";
System.out.println(name+descritpion); 

輸出為:老馬說編程,探索編程本質

String類包括很多方法,以方便操作字符串。

判斷字符串是否為空

public boolean isEmpty()

獲取字符串長度

 public int length()

取子字符串

public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex) 

在字符串中查找字符或子字符串,返回第一個找到的索引位置,沒找到返回-1

public int indexOf(int ch)
public int indexOf(String str)

從後面查找字符或子字符串,返回從後面數的第一個索引位置,沒找到返回-1

public int lastIndexOf(int ch)
public int lastIndexOf(String str) 

判斷字符串中是否包含指定的字符序列。回顧一下,CharSequence是一個接口,String也實現了CharSequence

public boolean contains(CharSequence s)  

判斷字符串是否以給定子字符串開頭

public boolean startsWith(String prefix)

判斷字符串是否以給定子字符串結尾

public boolean endsWith(String suffix)

與其他字符串比較,看內容是否相同

public boolean equals(Object anObject)

忽略大小寫,與其他字符串進行比較,看內容是否相同

public boolean equalsIgnoreCase(String anotherString)

String也實現了Comparable接口,可以比較字符串大小

public int compareTo(String anotherString)

還可以忽略大小寫,進行大小比較

public int compareToIgnoreCase(String str)

所有字符轉換為大寫字符,返回新字符串,原字符串不變

public String toUpperCase()

所有字符轉換為小寫字符,返回新字符串,原字符串不變

public String toLowerCase()

字符串連接,返回當前字符串和參數字符串合並後的字符串,原字符串不變

public String concat(String str)

字符串替換,替換單個字符,返回新字符串,原字符串不變

public String replace(char oldChar, char newChar)

字符串替換,替換字符序列,返回新字符串,原字符串不變

public String replace(CharSequence target, CharSequence replacement) 

刪掉開頭和結尾的空格,返回新字符串,原字符串不變

public String trim() 

分隔字符串,返回分隔後的子字符串數組,原字符串不變

public String[] split(String regex)

例如,按逗號分隔"hello,world":

String str = "hello,world";
String[] arr = str.split(",");

arr[0]為"hello", arr[1]為"world"。

從調用者的角度理解了String的基本用法,下面我們進一步來理解String的內部。

走進String內部

封裝字符數組

String類內部用一個字符數組表示字符串,實例變量定義為:

private final char value[];

String有兩個構造方法,可以根據char數組創建String

public String(char value[])
public String(char value[], int offset, int count)

需要說明的是,String會根據參數新創建一個數組,並拷貝內容,而不會直接用參數中的字符數組。

String中的大部分方法,內部也都是操作的這個字符數組。比如說:

  • length()方法返回的就是這個數組的長度
  • substring()方法就是根據參數,調用構造方法String(char value[], int offset, int count)新建了一個字符串
  • indexOf查找字符或子字符串時就是在這個數組中進行查找

這些方法的實現大多比較直接,我們就不贅述了。

String中還有一些方法,與這個char數組有關:

返回指定索引位置的char

public char charAt(int index)

返回字符串對應的char數組

public char[] toCharArray()

注意,返回的是一個拷貝後的數組,而不是原數組。

將char數組中指定范圍的字符拷貝入目標數組指定位置

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) 

按Code Point處理字符

與Character類似,String類也提供了一些方法,按Code Point對字符串進行處理。

public int codePointAt(int index)
public int codePointBefore(int index)
public int codePointCount(int beginIndex, int endIndex)
public int offsetByCodePoints(int index, int codePointOffset)

這些方法與我們在剖析Character一節介紹的非常類似,本節就不再贅述了。

編碼轉換

String內部是按UTF-16BE處理字符的,對BMP字符,使用一個char,兩個字節,對於增補字符,使用兩個char,四個字節。我們在第六節介紹過各種編碼,不同編碼可能用於不同的字符集,使用不同的字節數目,和不同的二進制表示。如何處理這些不同的編碼呢?這些編碼與Java內部表示之間如何相互轉換呢?

Java使用Charset這個類表示各種編碼,它有兩個常用靜態方法:

public static Charset defaultCharset()
public static Charset forName(String charsetName) 

第一個方法返回系統的默認編碼,比如,在我的電腦上,執行如下語句:

System.out.println(Charset.defaultCharset().name());

輸出為UTF-8

第二方法返回給定編碼名稱的Charset對象,與我們在第六節介紹的編碼相對應,其charset名稱可以是:US-ASCII, ISO-8859-1, windows-1252, GB2312, GBK, GB18030, Big5, UTF-8,比如:

Charset charset = Charset.forName("GB18030");

String類提供了如下方法,返回字符串按給定編碼的字節表示:

public byte[] getBytes()  
public byte[] getBytes(String charsetName)
public byte[] getBytes(Charset charset) 

第一個方法沒有編碼參數,使用系統默認編碼,第二方法參數為編碼名稱,第三個為Charset。

String類有如下構造方法,可以根據字節和編碼創建字符串,也就是說,根據給定編碼的字節表示,創建Java的內部表示。

public String(byte bytes[])
public String(byte bytes[], int offset, int length)
public String(byte bytes[], int offset, int length, String charsetName)
public String(byte bytes[], int offset, int length, Charset charset)
public String(byte bytes[], String charsetName)
public String(byte bytes[], Charset charset)

除了通過String中的方法進行編碼轉換,Charset類中也有一些方法進行編碼/解碼,本節就不介紹了。重要的是認識到,Java的內部表示與各種編碼是不同的,但可以相互轉換。

不可變性

與包裝類類似,String類也是不可變類,即對象一旦創建,就沒有辦法修改了。String類也聲明為了final,不能被繼承,內部char數組value也是final的,初始化後就不能再變了。

String類中提供了很多看似修改的方法,其實是通過創建新的String對象來實現的,原來的String對象不會被修改。比如說,我們來看concat()方法的代碼:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

通過Arrays.copyOf方法創建了一塊新的字符數組,拷貝原內容,然後通過new創建了一個新的String。關於Arrays類,我們將在後續章節詳細介紹。

與包裝類類似,定義為不可變類,程序可以更為簡單、安全、容易理解。但如果頻繁修改字符串,而每次修改都新建一個字符串,性能太低,這時,應該考慮Java中的另兩個類StringBuilder和StringBuffer,我們在下節介紹它們。

常量字符串

Java中的字符串常量是非常特殊的,除了可以直接賦值給String變量外,它自己就像一個String類型的對象一樣,可以直接調用String的各種方法。我們來看代碼:

System.out.println("老馬說編程".length());
System.out.println("老馬說編程".contains("老馬"));
System.out.println("老馬說編程".indexOf("編程"));

實際上,這些常量就是String類型的對象,在內存中,它們被放在一個共享的地方,這個地方稱為字符串常量池,它保存所有的常量字符串,每個常量只會保存一份,被所有使用者共享。當通過常量的形式使用一個字符串的時候,使用的就是常量池中的那個對應的String類型的對象。

比如說,我們來看代碼:

String name1 = "老馬說編程";
String name2 = "老馬說編程";
System.out.println(name1==name2);

輸出為true,為什麼呢?可以認為,"老馬說編程"在常量池中有一個對應的String類型的對象,我們假定名稱為laoma,上面代碼實際上就類似於:

String laoma = new String(new char[]{'老','馬','說','編','程'});
String name1 = laoma;
String name2 = laoma;
System.out.println(name1==name2);

實際上只有一個String對象,三個變量都指向這個對象,name1==name2也就不言而喻了。

需要注意的是,如果不是通過常量直接賦值,而是通過new創建的,==就不會返回true了,看下面代碼:

String name1 = new String("老馬說編程");
String name2 = new String("老馬說編程");
System.out.println(name1==name2);

輸出為false,為什麼呢?上面代碼類似於:

String laoma = new String(new char[]{'老','馬','說','編','程'});
String name1 = new String(laoma);
String name2 = new String(laoma);
System.out.println(name1==name2);

String類中以String為參數的構造方法代碼如下:

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

hash是String類中另一個實例變量,表示緩存的hashCode值,我們待會介紹。

可以看出, name1和name2指向兩個不同的String對象,只是這兩個對象內部的value值指向相同的char數組。其內存布局大概如下所示:


所以,name1==name2是不成立的,但name1.equals(name2)是true。

hashCode

我們剛剛提到hash這個實例變量,它的定義如下:

private int hash; // Default to 0

它緩存了hashCode()方法的值,也就是說,第一次調用hashCode()的時候,會把結果保存在hash這個變量中,以後再調用就直接返回保存的值。

我們來看下String類的hashCode方法,代碼如下:

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;
}

如果緩存的hash不為0,就直接返回了,否則根據字符數組中的內容計算hash,計算方法是:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

s表示字符串,s[0]表示第一個字符,n表示字符串長度,s[0]*31^(n-1)表示31的n-1次方再乘以第一個字符的值。

為什麼要用這個計算方法呢?這個式子中,hash值與每個字符的值有關,每個位置乘以不同的值,hash值與每個字符的位置也有關。使用31大概是因為兩個原因,一方面可以產生更分散的散列,即不同字符串hash值也一般不同,另一方面計算效率比較高,31*h與32*h-h即 (h<<5)-h等價,可以用更高效率的移位和減法操作代替乘法操作。

在Java中,普遍采用以上思路來實現hashCode。

正則表達式

String類中,有一些方法接受的不是普通的字符串參數,而是正則表達式,什麼是正則表達式呢?它可以理解為一個字符串,但表達的是一個規則,一般用於文本的匹配、查找、替換等,正則表達式有著豐富和強大的功能,是一個比較龐大的話題,我們將在後續章節單獨介紹。

Java中有專門的類如Pattern和Matcher用於正則表達式,但對於簡單的情況,String類提供了更為簡潔的操作,String中接受正則表達式的方法有:

分隔字符串

public String[] split(String regex) 

檢查是否匹配

public boolean matches(String regex)

字符串替換

public String replaceFirst(String regex, String replacement)
public String replaceAll(String regex, String replacement) 

小結

本節,我們介紹了String類,介紹了其基本用法,內部實現,編碼轉換,分析了其不可變性,常量字符串,以及hashCode的實現。

本節中,我們提到,在頻繁的字符串修改操作中,String類效率比較低,我們提到了StringBuilder和StringBuffer類。我們也看到String可以直接使用+和+=進行操作,它們的背後也是StringBuilder類。

讓我們下節來看下這兩個類。

----------------

未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。

-----------

相關好評原創文章

計算機程序的思維邏輯 (6) - 如何從亂碼中恢復 (上)?

計算機程序的思維邏輯 (7) - 如何從亂碼中恢復 (下)?

計算機程序的思維邏輯 (8) - char的真正含義

計算機程序的思維邏輯 (28) - 剖析包裝類 (下)

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved