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個字元,在國際不通用。
編碼LATIN1UTF8UTF16GB18030
長度定長為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%。

碼字不易,如有建議請掃碼Java字串編碼介紹


相關文章