依照Java的文檔, Java中的字符內部是以UTF-16編碼方式表示的,最小值是 \u0000
(0
),最大值是\uffff
(65535
), 也就是一個字符以2個字節來表示,難道Java最多只能表示 65535個字符?
char: The char data type is a single 16-bit Unicode character. It has a minimum value of '\u0000' (or 0) and a maximum value of '\uffff' (or 65,535 inclusive).
from The Java™ Tutorials
首先,讓我們先看個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class Main {
public static void main(String[] args) {
// 中文常見字
String s =
"你好";
System.out.println(
"1. string length =" + s.length());
System.out.println(
"1. string bytes length =" + s.getBytes().length);
System.out.println(
"1. string char length =" + s.toCharArray().length);
System.out.println();
// emojis
s =
"👦👩";
System.out.println(
"2. string length =" + s.length());
System.out.println(
"2. string bytes length =" + s.getBytes().length);
System.out.println(
"2. string char length =" + s.toCharArray().length);
System.out.println();
// 中文生僻字
s =
"𡃁妹";
System.out.println(
"3. string length =" + s.length());
System.out.println(
"3. string bytes length =" + s.getBytes().length);
System.out.println(
"3. string char length =" + s.toCharArray().length);
System.out.println();
}
}
|
運行這個程序,你覺得輸出結果是什么?
輸出結果:
1
2
3
4
5
6
7
8
9
10
11
|
1. string length =2
1. string bytes length =6
1. string char length =2
2. string length =4
2. string bytes length =8
2. string char length =4
3. string length =3
3. string bytes length =7
3. string char length =3
|
我們知道, String.getBytes()
如果不指定編碼格式,Java會使用操作系統的編碼格式得到字節數組,在我的MacOS中,默認使用UTF-8
作為字符編碼(locale
命令可以查看操作系統的編碼),所以在我的機器運行,String.getBytes()
會返回UTF-8
編碼的字節數組。
String.length
返回Unicode code units
的長度。
String.toCharArray
返回字符數組。
我們設置的字符串都是兩個unicode字符,輸出結果:
- 普通的中文字:字符串的長度是2,每個中文字按
UTF-8
編碼是三個字節,字符數組的長度看起來也沒問題 - emojis字符: 我們設置了兩個emojis字符,男女頭像。結果字符串的長度是4,
UTF-8
編碼8個字節,字符數組的長度是4 - 生僻的中文字:我們設置了兩個中文字,其中一個是生僻的中文字。結果字符串的長度是3,
UTF-8
編碼7個字節,字符數組的長度是3
看起來字符串的字符數和我們預期的有點不一樣,我們的字符串只有兩個unicode字符, 可是輸出結果有時候是2,有時候是3, 有時候是4,為什么呢?
這還得從Java的歷史說起。
Java最初設計的Charactor
用兩個字節來表示unicode字符,這沒有問題, 因為最初unicode中的字符還比較少, Java 1.1之前采用Unicode version 1.1.5
, JDK 1.1中支持Unicode 2.0
, JDK 1.1.7支持Unicode 2.1
, Java SE 1.4 支持 Unicode 3.0
, Java SE 5.0開始支持Unicode 4.0
。
直到Unicode 3.0
, Java用兩個字節來表示unicode字符還沒有問題,因為Unicode 3.0
最多49,259個字符, 兩個字節可以表示65,535個字符,還足夠容的下所有的uicode3.0字符。
但是Unicode 4.0
(事實上自Unicode 3.1), 字符集進行很大的擴充,已經達到了96,447個字符,Unicode 11.0
已經包含137,374個字符。
在Unicode中,為每一個字符對應一個編碼點(一個整數),用 U+緊跟着十六進制數表示。所有字符按照使用上的頻繁度划分為 17 個平面(編號為 0-16),即基本的多語言平面和增補平面。基本的多語言平面(英文為 Basic Multilingual Plane,簡稱 BMP)又稱平面 0,收集了使用最廣泛的字符。
這樣一來,Java的Charactor的兩個字節的設計,已經不足以容納所有的Unicode 4的字符, 所以可能需要4個字節才能表示擴展字符,所以現在的Charactor代表的已經不再是一個字符 (代碼點 code point), 而是一個代碼單元(code unit)。
-
Code Point: 代碼點,一個字符的數字表示。一個字符集一般可以用一張或多張由多個行和多個列所構成的二維表來表示。二維表中行與列交叉的點稱之為代碼點,每個碼點分配一個唯一的編號數字,稱之為碼點值或碼點編號,除開某些特殊區域(比如代理區、專用區)的非字符代碼點和保留代碼點,每個代碼點唯一對應於一個字符。 從
U+0000
到U+10FFFF
。 -
Code Unit:代碼單元,是指一個已編碼的文本中具有最短的比特組合的單元。對於 UTF-8 來說,代碼單元是 8 比特長;對於 UTF-16 來說,代碼單元是 16 比特長。換一種說法就是 UTF-8 的是以一個字節為最小單位的,UTF-16 是以兩個字節為最小單位的。
Java的字符在內部以UTF-16編碼方式來表示,String.length
返回的是Code Unit
的長度,而不再是Unicode中字符的長度。對於傳統的BMP
平面的代碼點,String.length
和我們傳統理解的字符的數量是一致的,對於擴展的字符,String.length
可能是我們理解的字符長度的兩倍。
有可能你會問, 對於一個UTF-16編碼的擴展字符,它以4個字節來表示,那么前兩個字節會不會和BMP
平面沖突,導致程序不知道它是擴展字符還是BMP
平面的字符?
其實是不會的, 幸運的是, 在BMP
平面中, U+D800
到U+DFFF
之間的碼位是永久保留不映射到Unicode字符,UTF-16就利用保留下來的0xD800-0xDFFF區塊的碼位來對輔助平面的字符的碼位進行編碼。
UTF-16編碼中,輔助平面中的碼位從U+10000
到U+10FFFF
,共計FFFFF個,需要20位來表示。第一個整數(兩個字節,稱為前導代理)要容納上述20位的前10位,第二個整數(稱為后尾代理)容納上述20位的后10位。前導代理的值的范圍是0xD800
到0xDBFF
,后尾代理的0xDC00
~0xDFFF
。可以看到前導代理和后尾代理的范圍都落在了BMP
平面中不用來映射的碼位,所以不會產生沖突,而且前導代理和后尾代理也沒有重合。這樣我們得到兩個字節的,就可以直接判斷它是否是BMP
平面的字符,還是擴展字符中的前導代理還是后尾代碼。
國外的有些用戶用emojis字符做自己的昵稱,導致有些系統不能正確的顯示出來,這是因為這些系統粗暴的使用Charactor
來表示,在顯示的時候截斷的時候有時候可能不是在正確的代碼點上進行截斷。
我們在進行字符串截取的時候,比如String.substring
有可能會踩到一些坑,尤其經常使用的emojis字符。
自 Java 1.5 java.lang.String就提供了Code Point
方法, 用來獲取完整的Unicode字符和Unicode字符數量:
- public int codePointAt(int index)
- public int codePointBefore(int index)
- public int codePointCount(int beginIndex, int endIndex)
注意這些方法中的index使用的是code unit
值。
參考文檔
- https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
- https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Character.html
- http://www.oracle.com/us/technologies/java/supplementary-142654.html
- https://stackoverflow.com/questions/2533097/java-unicode-encoding
- https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html
- https://zh.wikipedia.org/wiki/Unicode
- https://codeahoy.com/2016/05/08/the-char-type-in-java-is-broken/
- https://zh.wikipedia.org/wiki/UTF-16
- https://wiki.sei.cmu.edu/confluence/display/java/STR50-J.+Use+the+appropriate+method+for+counting+characters+in+a+string
- http://stn.audible.com/abcs-of-unicode/#common-unicode-mistakes-in-java-apps