Java字符串編碼


String

在Java中,String是一個引用類型,它本身也是一個class。但是,Java編譯器對String有特殊處理,即可以直接用"..."來表示一個字符串:

String s1 = "Hello!";

實際上字符串在String內部是通過一個char[]數組表示的,因此,按下面的寫法也是可以的:

String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'}); 

因為String太常用了,所以Java提供了"..."這種字符串字面量表示方法。

Java字符串的一個重要特點就是字符串不可變。這種不可變性是通過內部的private final char[]字段,以及沒有任何修改char[]的方法實現的。

我們來看一個例子:

// String

根據上面代碼的輸出,試解釋字符串內容是否改變。

字符串比較

當我們想要比較兩個字符串是否相同時,要特別注意,我們實際上是想比較字符串的內容是否相同。必須使用equals()方法而不能用==

我們看下面的例子:

// String

從表面上看,兩個字符串用==equals()比較都為true,但實際上那只是Java編譯器在編譯期,會自動把所有相同的字符串當作一個對象放入常量池,自然s1s2的引用就是相同的。

所以,這種==比較返回true純屬巧合。換一種寫法,==比較就會失敗:

// String

結論:兩個字符串比較,必須總是使用equals()方法。

要忽略大小寫比較,使用equalsIgnoreCase()方法。

String類還提供了多種方法來搜索子串、提取子串。常用的方法有:

// 是否包含子串: "Hello".contains("ll"); // true 

注意到contains()方法的參數是CharSequence而不是String,因為CharSequenceString的父類。

搜索子串的更多的例子:

"Hello".indexOf("l"); // 2 "Hello".lastIndexOf("l"); // 3 "Hello".startsWith("He"); // true "Hello".endsWith("lo"); // true 

提取子串的例子:

"Hello".substring(2); // "llo" "Hello".substring(2, 4); "ll" 

注意索引號是從0開始的。

去除首尾空白字符

使用trim()方法可以移除字符串首尾空白字符。空白字符包括空格,\t\r\n

" \tHello\r\n ".trim(); // "Hello" 

注意:trim()並沒有改變字符串的內容,而是返回了一個新字符串。

另一個strip()方法也可以移除字符串首尾空白字符。它和trim()不同的是,類似中文的空格字符\u3000也會被移除:

"\u3000Hello\u3000".strip(); // "Hello" " Hello ".stripLeading(); // "Hello " " Hello ".stripTrailing(); // " Hello" 

String還提供了isEmpty()isBlank()來判斷字符串是否為空和空白字符串:

"".isEmpty(); // true,因為字符串長度為0 " ".isEmpty(); // false,因為字符串長度不為0 " \n".isBlank(); // true,因為只包含空白字符 " Hello ".isBlank(); // false,因為包含非空白字符 

替換子串

要在字符串中替換子串,有兩種方法。一種是根據字符或字符串替換:

String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替換為'w' s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替換為"~~" 

另一種是通過正則表達式替換:

String s = "A,,B;C ,D"; s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D" 

上面的代碼通過正則表達式,把匹配的子串統一替換為","。關於正則表達式的用法我們會在后面詳細講解。

分割字符串

要分割字符串,使用split()方法,並且傳入的也是正則表達式:

String s = "A,B,C,D"; String[] ss = s.split("\\,"); // {"A", "B", "C", "D"} 

拼接字符串

拼接字符串使用靜態方法join(),它用指定的字符串連接字符串數組:

String[] arr = {"A", "B", "C"}; String s = String.join("***", arr); // "A***B***C" 

類型轉換

要把任意基本類型或引用類型轉換為字符串,可以使用靜態方法valueOf()。這是一個重載方法,編譯器會根據參數自動選擇合適的方法:

String.valueOf(123); // "123" String.valueOf(45.67); // "45.67" String.valueOf(true); // "true" String.valueOf(new Object()); // 類似java.lang.Object@636be97c 

要把字符串轉換為其他類型,就需要根據情況。例如,把字符串轉換為int類型:

int n1 = Integer.parseInt("123"); // 123 int n2 = Integer.parseInt("ff", 16); // 按十六進制轉換,255 

把字符串轉換為boolean類型:

boolean b1 = Boolean.parseBoolean("true"); // true boolean b2 = Boolean.parseBoolean("FALSE"); // false 

要特別注意,Integer有個getInteger(String)方法,它不是將字符串轉換為int,而是把該字符串對應的系統變量轉換為Integer

Integer.getInteger("java.version"); // 版本號,11 

轉換為char[]

Stringchar[]類型可以互相轉換,方法是:

char[] cs = "Hello".toCharArray(); // String -> char[] String s = new String(cs); // char[] -> String 

如果修改了char[]數組,String並不會改變:

// String <-> char[]

這是因為通過new String(char[])創建新的String實例時,它並不會直接引用傳入的char[]數組,而是會復制一份,所以,修改外部的char[]數組不會影響String實例內部的char[]數組,因為這是兩個不同的數組。

String的不變性設計可以看出,如果傳入的對象有可能改變,我們需要復制而不是直接引用。

例如,下面的代碼設計了一個Score類保存一組學生的成績:

// int[]
import java.util.Arrays;

觀察兩次輸出,由於Score內部直接引用了外部傳入的int[]數組,這會造成外部代碼對int[]數組的修改,影響到Score類的字段。如果外部代碼不可信,這就會造成安全隱患。

請修復Score的構造方法,使得外部代碼對數組的修改不影響Score實例的int[]字段。

字符編碼

在早期的計算機系統中,為了給字符編碼,美國國家標准學會(American National Standard Institute:ANSI)制定了一套英文字母、數字和常用符號的編碼,它占用一個字節,編碼范圍從0127,最高位始終為0,稱為ASCII編碼。例如,字符'A'的編碼是0x41,字符'1'的編碼是0x31

如果要把漢字也納入計算機編碼,很顯然一個字節是不夠的。GB2312標准使用兩個字節表示一個漢字,其中第一個字節的最高位始終為1,以便和ASCII編碼區分開。例如,漢字'中'GB2312編碼是0xd6d0

類似的,日文有Shift_JIS編碼,韓文有EUC-KR編碼,這些編碼因為標准不統一,同時使用,就會產生沖突。

為了統一全球所有語言的編碼,全球統一碼聯盟發布了Unicode編碼,它把世界上主要語言都納入同一個編碼,這樣,中文、日文、韓文和其他語言就不會沖突。

Unicode編碼需要兩個或者更多字節表示,我們可以比較中英文字符在ASCIIGB2312Unicode的編碼:

英文字符'A'ASCII編碼和Unicode編碼:

         ┌────┐
ASCII:   │ 41 │
         └────┘
         ┌────┬────┐
Unicode: │ 00 │ 41 │
         └────┴────┘

英文字符的Unicode編碼就是簡單地在前面添加一個00字節。

中文字符'中'GB2312編碼和Unicode編碼:

         ┌────┬────┐
GB2312:  │ d6 │ d0 │
         └────┴────┘
         ┌────┬────┐
Unicode: │ 4e │ 2d │
         └────┴────┘

那我們經常使用的UTF-8又是什么編碼呢?因為英文字符的Unicode編碼高字節總是00,包含大量英文的文本會浪費空間,所以,出現了UTF-8編碼,它是一種變長編碼,用來把固定長度的Unicode編碼變成1~4字節的變長編碼。通過UTF-8編碼,英文字符'A'UTF-8編碼變為0x41,正好和ASCII碼一致,而中文'中'UTF-8編碼為3字節0xe4b8ad

UTF-8編碼的另一個好處是容錯能力強。如果傳輸過程中某些字符出錯,不會影響后續字符,因為UTF-8編碼依靠高字節位來確定一個字符究竟是幾個字節,它經常用來作為傳輸編碼。

在Java中,char類型實際上就是兩個字節的Unicode編碼。如果我們要手動把字符串轉換成其他編碼,可以這樣做:

byte[] b1 = "Hello".getBytes(); // 按ISO8859-1編碼轉換,不推薦 byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8編碼轉換 byte[] b2 = "Hello".getBytes("GBK"); // 按GBK編碼轉換 byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8編碼轉換 

注意:轉換編碼后,就不再是char類型,而是byte類型表示的數組。

如果要把已知編碼的byte[]轉換為String,可以這樣做:

byte[] b = ... String s1 = new String(b, "GBK"); // 按GBK轉換 String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8轉換 

始終牢記:Java的Stringchar在內存中總是以Unicode編碼表示。

延伸閱讀

對於不同版本的JDK,String類在內存中有不同的優化方式。具體來說,早期JDK版本的String總是以char[]存儲,它的定義如下:

public final class String { private final char[] value; private final int offset; private final int count; } 

而較新的JDK版本的String則以byte[]存儲:如果String僅包含ASCII字符,則每個byte存儲一個字符,否則,每兩個byte存儲一個字符,這樣做的目的是為了節省內存,因為大量的長度較短的String通常僅包含ASCII字符:

public final class String { private final byte[] value; private final byte coder; // 0 = LATIN1, 1 = UTF16 

對於使用者來說,String內部的優化不影響任何已有代碼,因為它的public方法簽名是不變的。

小結

  • Java字符串String是不可變對象;

  • 字符串操作不改變原字符串內容,而是返回新字符串;

  • 常用的字符串操作:提取子串、查找、替換、大小寫轉換等;

  • Java使用Unicode編碼表示Stringchar

  • 轉換編碼就是將Stringbyte[]轉換,需要指定編碼;

  • 轉換為byte[]時,始終優先考慮UTF-8編碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM