Java中 Character、String、StringBuilder 等類用于文本處理,它們的基礎都是 char。
字符編碼基礎
ASCII 碼
最高位設置為 0,用剩下的 7 位表示字符。這 7 位可以看作數(shù)字 0~127。
數(shù)字 32~126 表示的字符都是可打印字符,0~31 和 127 表示不可以打印的字符,這些字符一般用于控制目的,這些字符中大部分都是不常用的。數(shù)字 32~126 的含義,如圖2-1所示,除了中文之外,我們平常用的字符基本都涵蓋了,鍵盤上的字符大部分也都涵蓋了。
ISO 8859-1
最高位為1,ISO 8859-1又稱 Latin-1,它也是使用一個字節(jié)表示一個字符,其中 0~127 與 ASCII 一樣,128~255 規(guī)定了不同的含義。在128~255中,128~159 表示一些控制字符,這些字符也不常用,就不介紹了。160~255 表示一些西歐字符。
Windows-1252
ISO 8859-1 雖然號稱是標準,用于西歐國家,但它連歐元(€)這個符號都沒有,因為歐元比較晚,而標準比較早。實際中使用更為廣泛的是Windows-1252 編碼,這個編碼與 ISO 8859-1 基本是一樣的,區(qū)別只在于數(shù)字 128~159。Windows-1252 使用其中的一些數(shù)字表示可打印字符。這個編碼中加入了歐元符號以及一些其他常用的字符?;旧峡梢哉J為,ISO 8859-1 已被 Windows-1252 取代,在很多應用程序中,即使文件聲明它采用的是 ISO 8859-1編碼,解析的時候依然被當作 Windows-1252 編碼。
我國內(nèi)地的三個主要編碼 GB2312、GBK、GB18030 有時間先后關系,表示的字符數(shù)越來越多,且后面的兼容前面的,GB2312 和 GBK 都是用兩個字節(jié)表示,而 GB18030 則使用兩個或四個字節(jié)表示。
我國香港特別行政區(qū)和我國臺灣地區(qū)的主要編碼是 Big5。
如果文本里的字符都是 ASCII 碼字符,那么采用以上所說的任一編碼方式都是一樣的。
但如果有高位為 1 的字符,除了 GB2312、GBK、GB18030外,其他編碼都是不兼容的。比如,Windows-1252 和中文的各種編碼是不兼容的,即使 Big5 和 GB18030 都能表示繁體字,其表示方式也是不一樣的,而這就會出現(xiàn)所謂的亂碼,具體我們稍后介紹。
Unicode 編碼
Unicode 給世界上每個字符分配了一個編號,編號范圍為 0x000000~0x10FFFF。編號范圍在 0x0000~0xFFFF 的字符為常用字符集,即 65 536個數(shù)字之內(nèi),稱BMP(Basic Multilingual Plane)字符。編號范圍在 0x10000~0x10FFFF 的字符叫做增補字符(supplementary character)。每個字符都有一個Unicode編號,這個編號一般寫成十六進制,在前面加U+。大部分中文的編號范圍為 U+4E00~U+9FFF,例如,“馬”的 Unicode是 U+9A6C。
Unicode 主要規(guī)定了編號,但沒有規(guī)定如何把編號映射為二進制。UTF-16 是一種編碼方式,或者叫映射方式,它將編號映射為 2 或 4 個字節(jié),對 BMP 字符,它直接用 2 個字節(jié)表示,對于增補字符,使用 4 個字節(jié)表示,前兩個字節(jié)叫高代理項(high surrogate),范圍為 0xD800~0xDBFF,后兩個字節(jié)叫低代理項(low surrogate),范圍為 0xDC00~0xDFFF。UTF-16 定義了一個公式,可以將編號與 4 字節(jié)表示進行相互轉(zhuǎn)換。
Java 內(nèi)部采用 UTF-16 編碼,char 表示一個字符,但只能表示 BMP 中的字符,對于增補字符,需要使用兩個 char 表示,一個表示高代理項,一個表示低代理項。
那編號怎么對應到二進制表示呢?有多種方案,主要有UTF-32、UTF-16和UTF-8。
UTF-32
這個最簡單,就是字符編號的整數(shù)二進制形式,4 個字節(jié)。但有個細節(jié),就是字節(jié)的排列順序,如果第一個字節(jié)是整數(shù)二進制中的最高位,最后一個字節(jié)是整數(shù)二進制中的最低位,那這種字節(jié)序就叫“大端”(Big Endian, BE),否則,就叫“小端”(Little Endian, LE)。對應的編碼方式分別是 UTF-32BE 和 UTF-32LE。可以看出,每個字符都用 4 個字節(jié)表示,非常浪費空間,實際采用的也比較少。UTF-16
UTF-16使用變長字節(jié)表示:
1)對于編號在U+0000~U+FFFF的字符(常用字符集),直接用兩個字節(jié)表示。
2)字符值在 U+10000~U+10FFFF 的字符(也叫做增補字符集),需要用4個字節(jié)表示。前兩個字節(jié)叫高代理項,范圍是 U+D800~U+DBFF;后兩個字節(jié)叫低代理項,范圍是U+DC00~U+DFFF。數(shù)字編號和這個二進制表示之間有一個轉(zhuǎn)換算法,這里就不介紹了。
區(qū)分是兩個字節(jié)還是 4 個字節(jié)表示一個字符就看前兩個字節(jié)的編號范圍,如果是 U+D800~U+DBFF,就是4個字節(jié),否則就是兩個字節(jié)。
UTF-16 也有和 UTF-32 一樣的字節(jié)序問題,如果高位存放在前面就叫大端(BE),編碼就叫 UTF-16BE,否則就叫小端,編碼就叫UTF-16LE。
UTF-16 常用于系統(tǒng)內(nèi)部編碼,UTF-16 比 UTF-32 節(jié)省了很多空間,但是任何一個字符都至少需要兩個字節(jié)表示,對于美國和西歐國家而言,還是很浪費的。
- UTF-8
UTF-8 使用變長字節(jié)表示,每個字符使用的字節(jié)個數(shù)與其Unicode編號的大小有關,編號小的使用的字節(jié)就少,編號大的使用的字節(jié)就多,使用的字節(jié)個數(shù)為1~4不等。
小于128的,編碼與ASCII碼一樣,最高位為0。其他編號的第一個字節(jié)有特殊含義,最高位有幾個連續(xù)的1就表示用幾個字節(jié)表示,而其他字節(jié)都以10開頭。
char
char 看上去是很簡單的,char 用于表示一個字符,這個字符可以是中文字符,也可以是英文字符。賦值時把常量字符用單引號括起來。
在 Java 內(nèi)部進行字符處理時,采用的都是 Unicode,具體編碼格式是UTF-16BE。簡單回顧一下,UTF-16 使用 2 個或 4 個字節(jié)表示一個字符,Unicode 編號范圍在 65536 以內(nèi)的占兩個字節(jié),超出范圍的占4個字節(jié),BE 就是先輸出高位字節(jié),再輸出低位字節(jié),這與整數(shù)的內(nèi)存表示是一致的。
char 本質(zhì)上是一個固定占用兩個字節(jié)的無符號正整數(shù),這個正整數(shù)對應于 Unicode 編號,用于表示那個 Unicode 編號對應的字符。由于固定占用兩個字節(jié),char 只能表示 Unicode 編號在 65 536 以內(nèi)的字符,而不能表示超出范圍的字符。那超出范圍的字符怎么表示呢?使用兩個 char。
char 的賦值形式:
// 賦值的時候是按當前的編碼解讀方式,將這個字符形式對應的 Unicode 編號值賦給變量
char c = '蕾';
System.out.println(c);
// 賦值方式是按 Unicode 字符形式
c = '\u857e';
System.out.println(c);
// Unicode 編號的十六進制表示
c = 0x857e;
System.out.println(c);
// Unicode 編號的十進制表示
c = 34174;
System.out.println(c);
// Unicode 編號的二進制表示
c = 0b10000101_01111110;
System.out.println(c);
char 的加減運算就是按其 Unicode 編號進行運算,一般對字符做加減運算沒什么意義,但 ASCII 碼字符是有意義的。比如大小寫轉(zhuǎn)換,大寫A~Z的編號是 65~90,小寫 a~z 的編號是 97~122,正好相差 32,所以大寫轉(zhuǎn)小寫只需加 32,而小寫轉(zhuǎn)大寫只需減 32。加減運算的另一個應用是加密和解密,將字符進行某種可逆的數(shù)學運算可以做加解密。
java.lang.String 類
Java 中的字符串是由雙引號括起來的多個字符,下面示例都是表示字符串常量:
String str = "Hello World"
String str = "\u0048\u0065\u006c\u006c\u006f\u0020\u0057\u006f\u0072\u006c\u0064"
String str = "世界你好"
String str = "A"
String 常用的構造方法
String():使用空字符串創(chuàng)建并初始化一個新的 String 對象。
String(String original):使用另外一個字符串創(chuàng)建并初始化一個新的 String 對象。
String(StringBuffer buffer):使用可變字符串對象(StringBuffer)創(chuàng)建并初始化一個新的 String 對象。
String(StringBuilder builder):使用可變字符串對象(StringBuilder)創(chuàng)建并初始化一個新的 String 對象。
String(byte[] bytes):使用平臺的默認字符集解碼指定的 byte 數(shù)組,通過 byte 數(shù)組創(chuàng)建并初始化一個新的 String 對象。
String(char[] value):通過字符數(shù)組創(chuàng)建并初始化一個新的 String 對象。
String(char[] value, int offset, int count):通過字符數(shù)組的子數(shù)組創(chuàng)建并初始化一個新的 String 對象;offset參數(shù)是子數(shù)組第一個字符的索引,count 參數(shù)指定子數(shù)組的長度。
關于 String的實現(xiàn)原理,String 類內(nèi)部用一個字符數(shù)組表示字符串。在Java 9對String的實現(xiàn)進行了優(yōu)化,它的內(nèi)部不是 char 數(shù)組,而是 byte 數(shù)組,如果字符都是 ASCII 字符,它就可以使用一個字節(jié)表示一個字符,而不用 UTF-16BE 編碼,節(jié)省內(nèi)存。
String 的查找
在給定的字符串中查找字符或字符串是比較常見的操作。在 String 類中提供了 indexOf 和 lastIndexOf 方法用于查找字符或字符串,返回值是查找的字符或字符串所在的位置,-1 表示沒有找到。這兩個方法有多個重載版本:
int indexOf(int ch):從前往后搜索字符 ch,返回第一次找到字符 ch 所在處的索引。
int indexOf(int ch, int fromIndex):從指定的索引開始從前往后搜索字符 ch,返回第一次找到字符ch所在處的索引。
int indexOf(String str):從前往后搜索字符串 str,返回第一次找到字符串所在處的索引。
int indexOf(String str, int fromIndex):從指定的索引開始從前往后搜索字符串 str,返回第一次找到字符串所在處的索引。
int lastIndexOf(int ch):從后往前搜索字符 ch,返回第一次找到字符 ch 所在處的索引。
int lastIndexOf(int ch, int fromIndex):從指定的索引開始從后往前搜索字符ch,返回第一次找到字符ch所在處的索引。
int lastIndexOf(String str):從后往前搜索字符串 str,返回第一次找到字符串所在處的索引。
int lastIndexOf(String str, int fromIndex):從指定的索引開始從后往前搜索字符串 str,返回第一次找到字符串所在處的索引。
String 的比較
- 比較相等
String 提供的比較字符串相等的方法:
-
boolean equals(Object anObject):比較兩個字符串中內(nèi)容是否相等。 -
boolean equalsIgnoreCase(String anotherString):類似 equals 方法,只是忽略大小寫。
- 比較大小
有時不僅需要知道是否相等,還要知道大小,String 提供的比較大小的方法:
- int compareTo(String anotherString):按字典順序比較兩個字符串(字典中順序事實上就它的 Unicode 編碼)。如果參數(shù)字符串等于此字符串,則返回值 0;如果此字符串小于字符串參數(shù),則返回一個小于 0 的值;如果此字符串大于字符串參數(shù),則返回一個大于 0 的值。
- int compareToIgnoreCase(String str):類似 compareTo,只是忽略大小寫。
- 比較前綴和后綴
- boolean endsWith(String suffix):測試此字符串是否以指定的后綴結束。
- boolean startsWith(String prefix):測試此字符串是否以指定的前綴開始。
String 的字符串截取
String substring(int beginIndex):從指定索引 beginIndex 開始截取一直到字符串結束的子字符串。
String substring(int beginIndex, int endIndex):從指定索引 beginIndex 開始截取直到索引 endIndex - 1 處的字符,注意包括索引為 beginIndex 處的字符,但不包括索引為endIndex處的字符。
另外,String 還提供了字符串分割方法split(" ")方法,參數(shù)是分割字符串,返回值String[]。
trim() 返回一個前后不含任何空格的調(diào)用字符串的副本
String 的+和+=運算符
Java中,String 可以直接使用 + 和 += 運算符,這是 Java 編譯器提供的支持,背后,Java 編譯器一般會生成 StringBuilder, + 和 += 操作會轉(zhuǎn)換為 append。
對于簡單的情況,可以可以直接使用 String 的 + 和 +=,對于復雜的情況,尤其是有循環(huán)的時候,應該直接使用 StringBuilder。
可變字符串 StringBuffer 和 StringBuilder
Java 提供了兩個可變字符串類 StringBuffer 和 StringBuilder,中文翻譯為“字符串緩沖區(qū)”。
StringBuffer 是線程安全的,它的方法是支持線程同步,線程同步會操作串行順序執(zhí)行,在單線程環(huán)境下會影響效率。StringBuilder 是 StringBuffer 單線程版本,Java 5之后發(fā)布的,它不是線程安全的,但它的執(zhí)行效率很高。
StringBuffer 和 StringBuilder 具有完全相同的 API,即構造方法和方法等內(nèi)容一樣。StringBuilder 的中構造方法有4個:
StringBuilder():創(chuàng)建字符串內(nèi)容是空的 StringBuilder 對象,初始容量默認為 16個字符。
StringBuilder(CharSequence seq):指定 CharSequence 字符串創(chuàng)建StringBuilder 對象。CharSequence 接口類型,它的實現(xiàn)類有:String、StringBuffer 和 StringBuilder 等,所以參數(shù) seq 可以是String、StringBuffer 和 StringBuilder 等類型。
StringBuilder(int capacity):創(chuàng)建字符串內(nèi)容是空的 StringBuilder 對象,初始容量由參數(shù)capacity指定的。
StringBuilder(String str):指定String字符串創(chuàng)建 StringBuilder 對象。
StringBuffer 的追加、插入、刪除和替換
- 字符串追加方法是 append,append 有很多重載方法,可以追加任何類型數(shù)據(jù)。
- StringBuilder insert(int offset, String str):在字符串緩沖區(qū)中索引為 offset 的字符位置之前插入str,insert 有很多重載方法,可以插入任何類型數(shù)據(jù)。
- delete(int start, int end):在字符串緩沖區(qū)中刪除子字符串,要刪除的子字符串從指定索引 start 開始直到索引 end - 1 處的字符。start 和 end 兩個參數(shù)與 substring(int beginIndex, int endIndex)方法中的兩個參數(shù)含義一樣。
- replace(int start, int end, String str) 字符串緩沖區(qū)中用 str 替換子字符串,子字符串從指定索引 start 開始直到索引 end - 1 處的字符。start 和 end 同 delete(int start, int end)方法。
編碼轉(zhuǎn)換
String內(nèi)部是按 UTF-16BE 處理字符的,對 BMP 字符,使用一個 char,兩個字節(jié),對于增補字符,使用兩個 char,四個字節(jié)。不同編碼可能用于不同的字符集,使用不同的字節(jié)數(shù)目,以及不同的二進制表示。如何處理這些不同的編碼呢?這些編碼與 Java 內(nèi)部表示之間如何相互轉(zhuǎn)換呢?
Java 使用 Charset 類表示各種編碼,它有兩個常用靜態(tài)方法:Charset.defaultCharset() 和 Charset.forName(String charsetName)。
String 類提供了如下方法,返回字符串按給定編碼的字節(jié)表示:getByte(),getByte(String charsetName),getByte(Charset charset)。
字符串亂碼問題
亂碼有兩種常見原因:一種比較簡單,就是簡單的解析錯誤;另外一種比較復雜,在錯誤解析的基礎上進行了編碼轉(zhuǎn)換。
簡單的解析導致的亂碼,之所以看起來是亂碼,是因為看待或者說解析數(shù)據(jù)的方式錯了。只要使用正確的編碼方式進行解讀就可以糾正了。
如果怎么改變查看方式都不對,那很有可能就不僅僅是解析二進制的方式不對,而是文本在錯誤解析的基礎上還進行了編碼轉(zhuǎn)換。恢復的基本思路是嘗試進行逆向操作,假定按一種編碼轉(zhuǎn)換方式獲取亂碼的二進制格式,然后再假定一種編碼解讀方式解讀這個二進制,查看其看上去的形式,這要嘗試多種編碼,如果能找到看著正常的字符形式,應該就可以恢復。