1. 常見字串編碼
常見的字串編碼有:
- LATIN1
- 只能儲存ASCII字元,又稱ISO-8859-1。
- UTF-8
- 變長字元編碼,一個字元需要使用1個、2個或者3個byte表示。由於中文通常需要3個位元組表示,中文場景UTF-8編碼通常需要更多的空間,替代的方案是GBK/GB2312/GB18030。
- UTF-16
- 2個字元,一個字元需要使用2個byte表示,又稱UCS-2 (2-byte Universal Character Set)。根據大小端的區分,UTF-16有兩種形式,UTF-16BE和UTF-16LE,預設UTF-16指UTF-16BE。Java語言中的char是UTF-16LE編碼。
- GB18030
- 變長字元編碼,一個字元需要使用1個、2個或者3個byte表示。類似UTF8,但中文只需要2個字元,在國際不通用。
編碼 | LATIN1 | UTF8 | UTF16 | GB18030 |
---|---|---|---|---|
長度 | 定長為1 | 變長1/2/3 | 定長2 | 變長1/2/3 |
計算速度 | 快 | 慢 | 快 | 慢 |
英文儲存空間 | 小 | 小 | 大 | |
中文儲存空間 | 大 | 小 | 小 | |
典型場景 | 儲存常用編碼 | 計算常用編碼 | 中文儲存 |
為了計算方便,記憶體中字串通常使用等寬字元,Java語言中char和.NET中的char都是使用UTF-16。早期Windows-NT只支援UTF-16。
2. 編碼轉換效能
UTF-16和UTF-8之間轉換比較複雜,通常效能較差。
如下是一個將UTF-16轉換為UTF-8編碼的實現,可以看出演算法比較複雜,所以效能較差
static int encodeUTF8(char[] utf16, int off, int len, byte[] dest, int dp) {
int sl = off + len, last_offset = sl - 1;
while (off <
sl) {
char c = utf16[off++];
if (c <
0x80) {
// Have at most seven bits dest[dp++] = (byte) c;
} else if (c <
0x800) {
// 2 dest, 11 bits dest[dp++] = (byte) (0xc0 | (c >
>
6));
dest[dp++] = (byte) (0x80 | (c &
0x3f));
} else if (c >
= '\uD800' &
&
c <
'\uE000') {
int uc;
if (c <
'\uDC00') {
if (off >
last_offset) {
dest[dp++] = (byte) '?';
return dp;
} char d = utf16[off];
if (d >
= '\uDC00' &
&
d <
'\uE000') {
uc = (c <
<
10) + d + 0xfca02400;
} else {
throw new RuntimeException("encodeUTF8 error", new MalformedInputException(1));
}
} else {
uc = c;
} dest[dp++] = (byte) (0xf0 | ((uc >
>
18)));
dest[dp++] = (byte) (0x80 | ((uc >
>
12) &
0x3f));
dest[dp++] = (byte) (0x80 | ((uc >
>
6) &
0x3f));
dest[dp++] = (byte) (0x80 | (uc &
0x3f));
off++;
// 2 utf16
} else {
// 3 dest, 16 bits dest[dp++] = (byte) (0xe0 | ((c >
>
12)));
dest[dp++] = (byte) (0x80 | ((c >
>
6) &
0x3f));
dest[dp++] = (byte) (0x80 | (c &
0x3f));
}
} return dp;
}複製程式碼
由於Java中char是UTF-16LE編碼,如果需要將char[]轉換為UTF-16LE編碼的byte[]時,可以使用sun.misc.Unsafe#copyMemory方法快速拷貝。比如:
static int writeUtf16LE(char[] chars, int off, int len, byte[] dest, final int dp) {
UNSAFE.copyMemory(chars , CHAR_ARRAY_BASE_OFFSET + off * 2 , dest , BYTE_ARRAY_BASE_OFFSET + dp , len * 2 );
dp += len * 2;
return dp;
}複製程式碼
3. Java String的編碼
不同版本的JDK String的實現不一樣,從而導致有不同的效能表現。char是UTF-16編碼,但String在JDK 9之後內部可以有LATIN1編碼。
3.1. JDK 6之前的String實現
class String {
char[] value;
int offset;
int count;
}複製程式碼
在Java 6之前,String.subString方法產生的String物件和原來String物件公用一個char[],這會導致引用subString會導致一個較大的char[]被引用而無法被GC回收。於是使得很多庫都會針對JDK 6及以下版本避免使用subString方法。
3.2. JDK 7/8的String實現
class String {
char[] value;
}複製程式碼
JDK 7之後,字串去掉了offset和count欄位,value.length就是原來的count。這避免了subString引用大char[]的問題,優化也更容易,從而JDK7/8中的String操作效能比Java 6有較大提升。
3.3. JDK 9/10/11的實現
class String {
byte code;
byte[] value;
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
}複製程式碼
JDK 9之後,value型別從char[]變成byte[],增加了一個欄位code,如果字元全部是ASCII字元,使用value使用LATIN編碼;如果存在任何一個非ASCII字元,則用UTF16編碼。這種混合編碼的方式,使得英文場景佔更少的記憶體。缺點是導致Java 9的String API效能可能不如JDK 8,有些場景下降10%。