Java字串編碼介紹

JavaDog發表於2019-01-27

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之間轉換比較複雜,通常效能較差。

image.png | left | 643x430

如下是一個將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%。

來源:https://juejin.im/post/5c4d5c43e51d457cba6cd8d2#comment

相關文章