為什么 Java 內(nèi)部使用 UTF-16 表示字符串嵌纲?

背景

許多年前 Unicode 的提出者天真地以為 16 位定長的字符可以容納地球上所有仍具活力的文字截型,Java 設(shè)計(jì)者也深以為然。

參考 Unicode 設(shè)計(jì)贵试,Java 設(shè)計(jì)者認(rèn)為完全可以設(shè)計(jì)一個(gè)雙字節(jié)數(shù)據(jù)類型來表達(dá)所有 Unicode 字符,于是便有了今天的原始數(shù)據(jù)類型 char

但后來發(fā)現(xiàn) 65,536 個(gè)字符根本不足以表達(dá)所有文字壹无,Java 5.0 版本既要支持 Unicode 4.0 同時(shí)要保證向后兼容性,不得不開始使用 UTF-16 作為內(nèi)部編碼方式感帅,

UTF-16 編碼

Unicode 基本多文種平面(BMP U+0000 to U+FFFF)涵蓋了幾乎所有現(xiàn)代語言斗锭,以及繁多的特殊符號(hào),Java 允許使用單個(gè) char 來表示 BMP 內(nèi)的字符失球,此時(shí)的編碼值等于 Unicode 代碼點(diǎn)(code point)拒迅,這是Java 最初的Unicode 實(shí)現(xiàn),這種編碼方式又稱之為 UCS-2。

Enough talk, show me the code !

我們嘗試打印位于 BMP 平面內(nèi)的上箭頭符號(hào)璧微。

首先作箍,查詢得知上箭頭符號(hào)對(duì)應(yīng)的 code point 是 0x2191,直接賦值給 char然后打忧傲颉:

char ch = 0x2191; 
System.out.println(ch);

輸出:

那么胞得,如何表示輔助多文種平面(SMP U+010000 to U+10FFFF)內(nèi)的字符呢?

Unicode 從 BMP 平面保留兩片連續(xù)區(qū)域用于表示 SMP 平面內(nèi)的字符屹电,即可以繼續(xù)與 UCS-2 編碼保持兼容阶剑,又能減少空間浪費(fèi),畢竟使用 SMP 的場合并不多危号。

這兩片區(qū)域分別是 0xD800–0xDBFF (高代理區(qū)域)牧愁、0xDC00–0xDFFF (低代理區(qū)域),編碼方式如下:

  1. 將代碼點(diǎn)減去 0x10000外莲,僅保留低 20 位猪半;
  2. 將高 10 位加上 0xD800,得到高代理偷线;
  3. 將低 10 位加上 0xDC00磨确,得到低代理;

高代理和低代理共同組成一個(gè)代理串(Surrogate Pair)唯一地標(biāo)識(shí) Unicode SMP 平面上的任一代碼點(diǎn)声邦。

Enough talk, show me the code !

我們來試試打印 Emoji 笑臉

int lowBits = 0x1F600 - 0x10000;

// 由于char 的長度為 16 位乏奥,采用代理對(duì)方式表示(surrogate pair)必須使用兩個(gè) char,并使用 String 包裝
char highSurrogate = (char) ((lowBits >> 10) + 0xD800);
char lowSurrogate = (char) ((lowBits & 0x3FF) + 0xDC00);
System.out.println(new String(new char[]{highSurrogate, lowSurrogate}));

輸出:

??

Java Character 類提供很豐富的靜態(tài)方法實(shí)現(xiàn) Unicode 相關(guān)操作亥曹,如下所見:

// 將代理對(duì)轉(zhuǎn)成對(duì)應(yīng) Unicode code point
Character.toCodePoint(char high, char low)

// 判斷 code point 所需字符數(shù)
Character.charCount(int codePoint)

// 判斷 code point 是否合法


// 判斷是否為高位代理(High Surrogate)
Character.isHighSurrogate(char ch)


// 獲取高位代理(High Surrogate)
Character.highSurrogate(char ch)


// 判斷是否為低位代理(Low Surrogate)
Character.isLowSurrogate(char ch)

// 獲取低位代理(Low Surrogate)
Character.lowSurrogate(char ch)

UTF-16 轉(zhuǎn)換 UTF-8

Java String 類支持任意編碼方式轉(zhuǎn)換邓了,其中就包括 UTF-8 編碼:

String.getBytes("UTF-8")

但該方法缺點(diǎn)也很明顯,無法重用已有的 buffer媳瞪,有些場合下可能十分不便驶悟。下面是 Google 實(shí)現(xiàn)的UTF-8 編碼方法,可以供大家參考:

public class GoogleUTF8 {
    public static int encodeUtf8(CharSequence in, byte[] out, int offset, int length) {
      int utf16Length = in.length();
      int j = offset;
      int i = 0;
      int limit = offset + length;
      // Designed to take advantage of
      // https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination
      for (char c; i < utf16Length && i + j < limit && (c = in.charAt(i)) < 0x80; i++) {
        out[j + i] = (byte) c;
      }
      if (i == utf16Length) {
        return j + utf16Length;
      }
      j += i;
      for (char c; i < utf16Length; i++) {
        c = in.charAt(i);
        if (c < 0x80 && j < limit) {
          out[j++] = (byte) c;
        } else if (c < 0x800 && j <= limit - 2) { // 11 bits, two UTF-8 bytes
          out[j++] = (byte) ((0xF << 6) | (c >>> 6));
          out[j++] = (byte) (0x80 | (0x3F & c));
        } else if ((c < Character.MIN_SURROGATE || Character.MAX_SURROGATE < c) && j <= limit - 3) {
          // Maximum single-char code point is 0xFFFF, 16 bits, three UTF-8 bytes
          out[j++] = (byte) ((0xF << 5) | (c >>> 12));
          out[j++] = (byte) (0x80 | (0x3F & (c >>> 6)));
          out[j++] = (byte) (0x80 | (0x3F & c));
        } else if (j <= limit - 4) {
          // Minimum code point represented by a surrogate pair is 0x10000, 17 bits,
          // four UTF-8 bytes
          final char low;
          if (i + 1 == in.length()
                  || !Character.isSurrogatePair(c, (low = in.charAt(++i)))) {
            throw new UnpairedSurrogateException((i - 1), utf16Length);
          }
          int codePoint = Character.toCodePoint(c, low);
          out[j++] = (byte) ((0xF << 4) | (codePoint >>> 18));
          out[j++] = (byte) (0x80 | (0x3F & (codePoint >>> 12)));
          out[j++] = (byte) (0x80 | (0x3F & (codePoint >>> 6)));
          out[j++] = (byte) (0x80 | (0x3F & codePoint));
        } else {
          // If we are surrogates and we're not a surrogate pair, always throw an
          // UnpairedSurrogateException instead of an ArrayOutOfBoundsException.
          if ((Character.MIN_SURROGATE <= c && c <= Character.MAX_SURROGATE)
              && (i + 1 == in.length()
                  || !Character.isSurrogatePair(c, in.charAt(i + 1)))) {
            throw new UnpairedSurrogateException(i, utf16Length);
          }
          throw new ArrayIndexOutOfBoundsException("Failed writing " + c + " at index " + j);
        }
      }
      return j;
    }
}

參考鏈接

  1. https://docs.oracle.com/javase/specs/jls/se6/html/lexical.html
  2. https://docs.oracle.com/javase/tutorial/i18n/text/unicode.html
  3. https://softwareengineering.stackexchange.com/questions/174947/why-does-java-use-utf-16-for-internal-string-representation
  4. https://www.oracle.com/technetwork/articles/javase/supplementary-142654.html
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末材失,一起剝皮案震驚了整個(gè)濱河市痕鳍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌龙巨,老刑警劉巖笼呆,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異旨别,居然都是意外死亡诗赌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門秸弛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铭若,“玉大人洪碳,你說我怎么就攤上這事〉鹜溃” “怎么了瞳腌?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長镜雨。 經(jīng)常有香客問我嫂侍,道長,這世上最難降的妖魔是什么荚坞? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任挑宠,我火速辦了婚禮,結(jié)果婚禮上颓影,老公的妹妹穿的比我還像新娘各淀。我一直安慰自己,他們只是感情好诡挂,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布碎浇。 她就那樣靜靜地躺著,像睡著了一般咆畏。 火紅的嫁衣襯著肌膚如雪南捂。 梳的紋絲不亂的頭發(fā)上吴裤,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天旧找,我揣著相機(jī)與錄音,去河邊找鬼麦牺。 笑死钮蛛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的剖膳。 我是一名探鬼主播魏颓,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吱晒!你這毒婦竟也來了甸饱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤仑濒,失蹤者是張志新(化名)和其女友劉穎叹话,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體墩瞳,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡驼壶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了喉酌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片热凹。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泵喘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出般妙,到底是詐尸還是另有隱情纪铺,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布股冗,位于F島的核電站霹陡,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏止状。R本人自食惡果不足惜烹棉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望怯疤。 院中可真熱鬧浆洗,春花似錦、人聲如沸集峦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽塔淤。三九已至摘昌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間高蜂,已是汗流浹背聪黎。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留备恤,地道東北人稿饰。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像露泊,于是被迫代替她去往敵國和親喉镰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容