1. 程式人生 > >字串轉Unicode額外2個位元組的來源

字串轉Unicode額外2個位元組的來源

論壇的討論地址如下:
http://topic.csdn.net/u/20081009/09/e899898c-591f-4985-ae88-5972475708fb.html


測試程式碼如下:
  1.    String s = "1";
  2. byte[] arr = s.getBytes("UNICODE");
  3.     System.out.println(Arrays.toString(arr)); // 12
感謝runshine的精到和火龍果、的精彩講解。


7樓
為了在讀取位元組時能知道所採用的位元組序,在傳輸時採用了一個名為
 “ZERO WIDTH NON-BREAKING SPACE”(U+FEFF)的字元用於限定位元組序,
開頭兩個位元組為 FE FF 時為 Big-Endian,為 FF FE 時為 Little-Endian。
詳見 RFC2781 3.2 節。

在 Java 中直接使用 Unicode 轉碼時會按照 UTF-16LE 的方式拆分,並加上 BOM。

如果採用 UTF-16 拆分,在 Java 中預設採用帶有 BOM 的 UTF-16BE 拆分。


11樓

Unicode規範中推薦的標記位元組順序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。

(Unicode是一種字元編碼方法,不過它是由國際組織設計,可以容納全世界所有語言文字的編碼方案。Unicode的學名是"Universal Multiple-Octet Coded Character Set",簡稱為UCS。UCS可以看作是"Unicode Character Set"的縮寫。)

在UCS編碼中有一個叫做"ZERO WIDTH NO-BREAK SPACE"的字元,它的編碼是FEFF。而FFFE在UCS中是不存在的字元,所以不應該出現在實際傳輸中。UCS規範建議在傳輸位元組流前,先傳輸字元"ZERO WIDTH NO-BREAK SPACE"。

這樣如果接收者收到FEFF,就表明這個位元組流是Big-Endian的;如果收到FFFE,就表明這個位元組流是Little-Endian的。因此字元"ZERO WIDTH NO-BREAK SPACE"又被稱作BOM。
在 Java 中直接使用Unicode 轉碼時會按照UTF-16LE 的方式拆分,並加上 BOM。 如果採用 UTF-16 拆分,在 Java 中預設採用帶有 BOM 的 UTF-16BE 拆分。 (其實Unicode與UTF-8是完全一樣的)


12樓

2個疑問,

1 為何UTF-8沒有
2 我想知道,在原始碼裡的哪個部分做的這個判斷。


19樓
引用 11 樓 lky5387 的回覆:
其實Unicode與UTF-8是完全一樣的

嗯,UTF-8 是通過 Unicode 轉換而來的,是 Unicode 的表現形式之一。


引用 12 樓 java2000_net 的回覆:
2個疑問,

1 為何UTF-8沒有
2 我想知道,在原始碼裡的哪個部分做的這個判斷。

只能回答你第一個問題:

UTF-8 是採用 1~4 個位元組來表示 Unicode 字元的,每個 Unicode 的 UTF-8 編碼的
第一個位元組是有一定範圍的,如果讀取到某個位元組的最高位為 0 那麼採用一個位元組表
示,如果最高位是兩個“1”就採用兩個位元組表示,最高位是三個“1”採用三個位元組表
示,以此類推。多位元組表示時,第二個和後面的位元組的最高位只能是“10”,也就是說
UTF-8 編碼時字元的第一個位元組的最高位不可能是“10”。

因此,UTF-8 只能採用 Big-Endian 的 BOM 方式。BOM 頭 U+FEFF,UTF-8 編碼為 EF BB BF
就穩含掉了。

如果採用 LE 方式時,位元組序顛倒了,第一個位元組最高位就是“10”了,這時 UTF-8 讀
取時會產生錯誤,這時會採用一個被稱為 Replacement Character (U+FFFD) 的字元來代替。

第二個問題正在看原始碼……


24樓


java.lang.String#getBytes(String) 的原始碼:
  1. publicbyte[] getBytes(String s) throws UnsupportedEncodingException {
  2. if(s == null)
  3. thrownew NullPointerException();
  4. else
  5. return StringCoding.encode(s, value, offset, count);
  6. }

java.lang.StringCoding.encode 的原始碼:
<!---->staticbyte[] encode(String s, char ac[], int i, int j) throws UnsupportedEncodingException { Object obj = (StringEncoder)deref(encoder); String s1 = s !=null? s : "ISO-8859-1"; if(obj ==null||!s1.equals(((StringEncoder) (obj)).requestedCharsetName()) &&!s1.equals(((StringEncoder) (obj)).charsetName())) { obj =null; try { Charset charset = lookupCharset(s1); if(charset !=null) obj =new CharsetSE(charset, s1); } catch(IllegalCharsetNameException illegalcharsetnameexception) { } if(obj ==null) obj =new ConverterSE(CharToByteConverter.getConverter(s1), s1); set(encoder, obj); } return ((StringEncoder) (obj)).encode(ac, i, j); } Unicode 編碼不存在對應的 Charset 因此會執行 new ConverterSE(CharToByteConverter.getConverter(s1), s1);
這一句。再通過其底層類庫的字符集類 sun.io.CharacterEncoding 可以找出 Unicode 的轉換器,
是採用 sun.io.CharToByteUnicode 這個類的,這個類的 sun.io 包是讀取 file.encoding.pkg 這
個系統屬性拼接字串反射而來的。

這個類是這樣拼出來的:
    file.encoding.pkg 屬性值 + "." + "CharToByte" + CharacterEncoding 中儲存的名字

這個類的構造是:
<!---->public CharToByteUnicode() { usesMark =true; markWritten =false; byteOrder =0; String s = (String)AccessController.doPrivileged(new GetPropertyAction("sun.io.unicode.encoding", "UnicodeBig")); if(s.equals("UnicodeBig")) byteOrder =1; elseif(s.equals("UnicodeLittle")) byteOrder =2; else byteOrder =1; } String s = (String)AccessController.doPrivileged(new GetPropertyAction("sun.io.unicode.encoding", "UnicodeBig"));
這一句很關鍵,決定著 Unicode 編碼時 byteOrder 的值是 1 還是 2。這裡有個 sun.io.unicode.encoding
這個系統屬性,在 Windows 系統下的值為:UnicodeLittle,因此 byteOrder 值為 2。

注:有蘋果機的同學用 System.out.println(System.getProperty("sun.io.unicode.encoding")); 看看這個值
是啥。

至於 Unicode 是怎麼轉換的看看 sun.io.CharToByteUnicode 這個類的原始碼就有了。


25樓
果子繼續,我想知道那個 -2 從哪裡來的!
我只看到了2


27樓, 果子有點“生氣”了,哈哈
汗,已經說了很清楚了啊,在 sun.io.CharToByteUnicode 這個類中啊。

貼上原始碼吧。

Java code <!---->01publicint convert(char ac[], int i, int j, byte abyte0[], int k, int l) throws ConversionBufferFullException, MalformedInputException { 02 charOff = i; 03 byteOff = k; 04if(i >= j) 05return0; 06int i1 = i; 07int j1 = k; 08int k1 = l -2; 09if(usesMark &&!markWritten) { 10if(j1 > k1) 11thrownew ConversionBufferFullException(); 12if(byteOrder ==1) { 13 abyte0[j1++] =-2; 14 abyte0[j1++] =-1; 15 } else { 16 abyte0[j1++] =-1; 17 abyte0[j1++] =-2; 18 } 19 markWritten =true; 20 } 21if(byteOrder ==1) 22while(i1 < j) { 23if(j1 > k1) { 24 charOff = i1; 25 byteOff = j1; 26thrownew ConversionBufferFullException(); 27 } 28char c = ac[i1++]; 29 abyte0[j1++] = (byte)(c >>8); 30 abyte0[j1++] = (byte)(c &255); 31 } 32else33while(i1 < j) { 34if(j1 > k1) { 35 charOff = i1; 36 byteOff = j1; 37thrownew ConversionBufferFullException(); 38 } 39char c1 = ac[i1++]; 40 abyte0[j1++] = (byte)(c1 &255); 41 abyte0[j1++] = (byte)(c1 >>8); 42 } 43 charOff = i1; 44 byteOff = j1; 45return j1 - k; 46 }

12 判斷位元組序,13~14 行寫入 BE 的 BOM;16~17 行寫入 LE 的 BOM。

22~31 以 BE 方式寫入位元組,33~42 以 LE 方式寫入位元組。
BE 和 LE 的寫入方式僅在 29~30 和 40~41 行不同。

29~30 為 BE 序先寫入高位元組,再寫入低位元組,而 40~41 為 LE 序,先寫低位元組,再寫高位元組的。


28樓
關於 Unicode 轉為 byte 陣列,做了個小實驗,呵呵。
<!---->publicclass Test1 { privatefinalstaticchar[] HEX ="0123456789abcdef".toCharArray(); publicstaticvoid main(String[] args) throws UnsupportedEncodingException { String str ="中國"; String[] encoding = { "Unicode", "UnicodeBig", "UnicodeLittle", "UnicodeBigUnmarked", "UnicodeLittleUnmarked", "UTF-16", "UTF-16BE", "UTF-16LE" }; for(int i =0; i < encoding.length; i++) { System.out.printf("%-22s %s%n", encoding[i], bytes2HexString(str.getBytes(encoding[i])) ); } } publicstatic String bytes2HexString(byte[] bys) { char[] chs =newchar[bys.length *2+ bys.length -1]; for(int i =0, offset =0; i < bys.length; i++) { if(i >0) { chs[offset++] =''; } chs[offset++] = HEX[bys[i] >>4&0xf]; chs[offset++] = HEX[bys[i] &0xf]; } returnnew String(chs); } } 輸出結果: Unicode ff fe 2d 4e fd 56 UnicodeBig fe ff 4e 2d 56 fd UnicodeLittle ff fe 2d 4e fd 56 UnicodeBigUnmarked 4e 2d 56 fd UnicodeLittleUnmarked 2d 4e fd 56 UTF-16 fe ff 4e 2d 56 fd UTF-16BE 4e 2d 56 fd UTF-16LE 2d 4e fd 56

總結:

1. Unicode 的位元組序方式採用 sun.io.unicode.encoding 的系統屬性,並且是加上 BOM 的。
    轉換類為:sun.io.CharToByteUnicode

2. UnicodeBig 位元組序為 BE 方式,加上 BOM
    轉換類為:sun.io.CharToByteUnicode 不過在 sun.io.CharToByteUnicodeBig 中強制使用 BE 序,
    即即構造時強制設定 byteOrder 值為 1

3. UnicodeLittle 位元組序為 LE 方式,加上 BOM
    轉換類為:sun.io.CharToByteUnicode 不過在 sun.io.CharToByteUnicodeLittle 中強制使用 LE 序,
    即即構造時強制設定 byteOrder 值為 2

4. UnicodeBigUnmarked 位元組序為 BE 方式,有 Unmarked 不加 BOM
    轉換類為:sun.io.CharToByteUnicode 不過在 sun.io.CharToByteUnicodeBigUnmarked 中強制使用 BE 序和不加 BOM。
    即構造時強制設定 byteOrder 值為 1,usesMark 值為 false

5. UnicodeLittleUnmarked 位元組序為 LE 方式,有 Unmarked 不加 BOM
    轉換類為:sun.io.CharToByteUnicode 不過在 sun.io.CharToByteUnicodeLittleUnmarked 中強制使用 LE 序和不加 BOM。
    即構造時強制設定 byteOrder 值為 2,usesMark 值為 false

6. UTF-16 位元組序為 BE 方式,加上 BOM
    轉換類為:sun.io.CharToByteUnicode 不過在 sun.io.CharToByteUTF16 中強制使用 BE 序和加 BOM。
    即構造時強制設定 byteOrder 值為 1,usesMark 值為 true
    UTF-16 實際上與 UnicodeBig 是一樣的。

7. UTF-16BE 沒有轉換類,與 UnicodeBigUnmarked 使用相同的轉換器。

8. UTF-16LE 沒有轉換類,與 UnicodeLittleUnmarked 使用相同的轉換器。


上面的那些類都是 sun.io.CharToByteUnicode 的子類,這些子類中就只有設定各種格式的構造方法。

根據 CharacterEncoding 的對映與 UnicodeBigUnmarked 相容的編碼有:unicode-1-1, iso-10646-ucs-2, utf-16be, x-utf-16be 這麼幾個。而且 UnicodeLittleUnmarked 相容的有:utf-16le, x-utf-16le 這兩個。



30樓
之前的話:
來遲了!但不可惜。呵呵。

輸出結果:
[-2, -1, 0, 49]

問題現象:
“意外”地多了兩個位元組,即陣列前兩個元素,其值分別為-2、-1

問題原因:
樓上各位已經說得非常清楚,詳見7樓與11樓

相關原始碼:
這是樓主最感興趣的內容,所以說得詳細一些。
String的encode()方法裡邊呼叫了StringCoding的encode()方法。
StringCoding的encode()方法有點複雜,包括快取機制、Charset物件的建立等,但主要是呼叫了其巢狀類StringEncoder的encode()方法。
而StringCoding.StringEncoder類的encode()方法又用到了CharsetEncoder類的encode()方法。
CharsetEncoder的encode()又呼叫了它自己的encodeLoop()方法。——這就是答案所在。換句話說,輸出BOM的原始碼就在CharsetEncoder的encodeLoop()方法中。
encodeLoop()方法是抽象方法,不同的子類有不同的實現。
針對於“Unicode”編碼,其對應的類為sun.nio.cs.UTF_16的巢狀類Encoder,以下是其原始碼:
Java code <!---->privatestaticclass Encoder extends UnicodeEncoder { public Encoder(Charset charset) { super(charset, 0, true); } }
以下是其父類UnicodeEncoder的部分原始碼:
Java code <!---->//構造方法protected UnicodeEncoder(Charset charset, int i, boolean flag) { super(charset, 2.0F, flag ? 4F : 2.0F, i !=0? (newbyte[] { -3, -1 }) : (newbyte[] { -1, -3 })); usesMark = needsMark = flag; byteOrder = i; } //最終完成編碼任務的方法——答案所在。protected CoderResult encodeLoop(CharBuffer charbuffer, ByteBuffer bytebuffer) { // 省略無關程式碼if (needsMark) { if (bytebuffer.remaining() <2) return CoderResult.OVERFLOW; put('\uFEFF', bytebuffer); needsMark =false; } // 省略無關程式碼 }
看完原始碼,再分析一下執行過程:
1、因為當前使用的字元編碼是“Unicode”,所以對應的CharsetEncoder是sun.nio.cs.UTF_16.Encoder
2、在建立UTF_16.Encoder的物件時,其傳給父類建構函式的第三個引數是true,意味著needsMark將被賦值為true
3、由於needsMark為true,在encodeLoop()執行的時候,會寫入0xFE、0xFF。然後needsMark被賦值為false,意味著BOM最多隻寫入一次
4、0xFE、0xFF轉換成byte,就分別是-2、-1

問題補充:
樓主又問到,為什麼使用“UTF-8”時不會加BOM?
參見第19樓。

而從原始碼的角度來說,因為“UTF-8”所對應的CharsetEncoder是sun.nio.cs.UTF_8.Encoder,
在這個類的原始碼中,encodeLoop()方法及與其的encodeArrayLoop()、encodeBufferLoop()方法的確沒有寫入BOM的操作,呵呵。

後面的話:
1、我的結論與27樓不太一致,27樓說的是sun.io.CharToByteUnicode,但我覺得我的跟蹤和分析過程是正確的,我是在JDK6下做的這個過程。呵呵。
2、強手如雲啊,學習了!呵呵。


再次感謝火龍果,欽佩他的研究精神和無私奉獻精神以及超強的忍耐力(我有時挺煩人的)。
感謝
感謝
也感謝所有的參與者,大家都提高了經驗值。