`
逆风的香1314
  • 浏览: 1453123 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

研究字符串转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的精到和lky5387火龙果、accp206的精彩讲解。


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. public byte[] getBytes(String s) throws UnsupportedEncodingException {
  2.     if(s == null)
  3.         throw new NullPointerException();
  4.     else
  5.         return StringCoding.encode(s, value, offset, count);
  6. }

java.lang.StringCoding.encode 的源代码:
<!---->static byte[] 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;
else
if(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
<!---->01 public int convert(char ac[], int i, int j, byte abyte0[], int k, int l) throws ConversionBufferFullException, MalformedInputException {
02 charOff = i;
03 byteOff = k;
04 if(i >= j)
05 return 0;
06 int i1 = i;
07 int j1 = k;
08 int k1 = l - 2;
09 if(usesMark && !markWritten) {
10 if(j1 > k1)
11 throw new ConversionBufferFullException();
12 if(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 }
21 if(byteOrder == 1)
22 while(i1 < j) {
23 if(j1 > k1) {
24 charOff = i1;
25 byteOff = j1;
26 throw new ConversionBufferFullException();
27 }
28 char c = ac[i1++];
29 abyte0[j1++] = (byte)(c >> 8);
30 abyte0[j1++] = (byte)(c & 255);
31 }
32 else
33 while(i1 < j) {
34 if(j1 > k1) {
35 charOff = i1;
36 byteOff = j1;
37 throw new ConversionBufferFullException();
38 }
39 char c1 = ac[i1++];
40 abyte0[j1++] = (byte)(c1 & 255);
41 abyte0[j1++] = (byte)(c1 >> 8);
42 }
43 charOff = i1;
44 byteOff = j1;
45 return 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 数组,做了个小实验,呵呵。
<!---->public class Test1 {

private final static char[] HEX = "0123456789abcdef".toCharArray();

public static void 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]))
);
}
}

public static String bytes2HexString(byte[] bys) {
char[] chs = new char[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];
}
return new 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
<!---->
private static class 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 ? (new byte[] {
-3, -1
}) : (
new byte[] {
-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、强手如云啊,学习了!呵呵。


再次感谢火龙果,钦佩他的研究精神和无私奉献精神以及超强的忍耐力(我有时挺烦人的)。
感谢 lky5387 的精彩发言
感谢accp206 在最后时刻的发言。

也感谢所有的参与者,大家都提高了经验值。

分享到:
评论

相关推荐

    ANSI字符串与Unicode字符串的相互转换

    由于Unicode字符串每个字符可能占用2字节,而ANSI字符串每个字符最多占用1字节,因此,转换可能导致内存需求的变化。 4. **异常处理**:转换可能会因为不正确的编码、内存不足或无效的输入而导致错误。因此,在实际...

    c# 字符串转 32位 16进制

    在C#中,字符串是Unicode字符的序列,每个字符由两个字节(16位)表示,可以是ASCII字符集中的单字节字符或者Unicode字符集中的双字节字符。如果要将字符串转换为32位16进制表示,我们需要对每个字符进行处理,并将...

    字符串转GB码, 和unicode码

    字符串转Unicode码则相对简单,因为Unicode是一个字符集,每个字符都有一个固定的码点。在Python中,字符串本身就是Unicode编码,因此,转换通常是指将其他编码(如GBK)的字符串转换为Unicode,这可以通过`decode`...

    双字节 多字节 宽字节 Unicode

    在双字节字符集中,低字节(第一个字节)被称为引导字节(Lead Byte),高字节(第二个字节)被称为后续字节(Trail Byte)。只有当两个字节一起出现时才构成一个完整的字符。这与单字节字符集中的每个字符都只占一...

    unicode字符串

    5. **C/C++中的Unicode支持**:C和C++标准库提供了对Unicode的支持,例如`wchar_t`类型用于宽字符,`std::wstring`用于宽字符串,但需要额外的库支持如Windows API中的`WideCharToMultiByte`和`MultiByteToWideChar`...

    gb18030与unicode转换表

    UTF-8是最常见的Unicode编码形式,它使用1到4个字节来表示一个字符,对于中文字符通常使用3个字节。 GB18030与Unicode之间的转换表,是为了在两个编码体系间进行数据交换而创建的工具。这个转换表列出了GB18030编码...

    FASM支持中文UNICODE字符串定义的宏

    除了转换字符串,处理UNICODE字符串时还需要注意内存对齐问题,因为UTF-16编码的每个字符占用2字节,可能导致内存的不规则分布。因此,在实际编程中,可能需要额外的步骤来确保数据的正确存储和读取。 总之,FASM...

    c#字符串转为base64

    1. **字符串与Base64编码**:在C#中,字符串通常由Unicode字符组成,而Base64则处理字节序列。因此,将字符串转换为Base64需要先将字符串转换为字节数组。这可以通过使用`System.Text.Encoding`类中的`GetBytes()`...

    如何将一个ansi的文本文件转为unicode格式

    `StringToWideChar`是Windows API中的一个函数,用于将多字节字符串(如ANSI)转换为宽字符字符串(Unicode)。这种方法在Windows平台上非常实用,因为它直接利用了操作系统级别的转换功能,能够处理各种复杂的编码...

    ASCII UNICODE 字符转换

    另一方面,UNICODE的多字节特性意味着在处理UNICODE文本时,需要额外注意字符边界,避免因误读字节顺序而导致的错误。 了解ASCII和UNICODE的转换对于开发跨语言、跨平台的应用至关重要。这涉及到对字符编码的理解,...

    统计字符串中英文标点数量并截取.zip

    GBK编码一个中文字符通常占用2个字节,而UTF-8编码下,中文字符一般占用3个字节,英文和数字字符通常只占1个字节。因此,在计算总长度时,需要根据字符串的实际编码方式来进行字节数的累加。 在数据库操作中,字段...

    C#将Unicode编码转换为汉字字符串的简单方法

    以下是一个示例方法,名为`ToGB2312`,用于将包含Unicode编码的字符串转换为汉字字符串: ```csharp public static string ToGB2312(string str) { // 使用正则表达式匹配Unicode编码和普通字符 MatchCollection ...

    关于VC++的字符串

    它总是以空字符终止,并且包含一个额外的字节作为字符串长度信息,这有助于提高性能和减少内存消耗。`BSTR`只能在Unicode环境下使用,因此其字符类型为`OLECHAR`,即`wchar_t`。 #### LPCTSTR:常量TCHAR指针 `...

    字符串数据结构实现(数组方式)

    例如,声明一个存储10个字符的字符串数组可以写为: ```c char str[10]; ``` 创建并初始化一个包含5个字符的字符串(包括结束符'\0')可以这样写: ```c strcpy(str, "hello"); ``` C++中,虽然也可以使用字符...

    C++11新特性探索(一)——原生字符串字面量问题

    通过研究这些文件,我们可以深入理解C++11原生字符串字面量的工作原理及其在实际项目中的应用。 总之,C++11的原生字符串字面量是提升代码可读性和兼容性的重要工具,尤其对于处理包含多种语言和字符集的字符串时。...

    字符串String MD5计算小工具 VC++篇

    在C++中,处理这两种字符串类型需要额外注意,因为Unicode字符串使用两个字节表示一个字符,而ANSI字符串通常只用一个字节。开发者可能使用了`_T`宏或者宽字符(`wchar_t`)来兼容两种编码方式。 `EncryptByMd5Dlg....

    Java Unicode 和字符集

    在Java中,字符串是以Unicode的形式存储的,这意味着Java的String类内部使用UTF-16编码来表示字符。Java通过使用`char`类型来表示单个Unicode字符,该类型占用16位,足够表示基本多文种平面上的所有字符。对于超出...

    java处理字符和字符串课件

    - **定义**:字符串是由零个或多个字符组成的有序集合。在Java中,字符串通过`String`类表示。 - **创建方式**: ```java String str = "hello"; String str2 = new String("world"); ``` - **字符串操作**: -...

    Vb-pointer-with-string-.rar_VB 字符串操作_vb字符串效率_vb指针字符串

    在VB6及其之前的版本中,字符串是Unicode编码的,每个字符占用两个字节。默认情况下,VB会使用变体数据类型(Variant)来处理字符串,这在处理小量数据时很方便,但在处理大量数据时可能会变得效率低下。这是因为...

    中文字符串截取(支持各种编码格式)底层实现源码

    2. **编码转换**:根据识别出的编码将二进制数据转换为Unicode字符串。Unicode是一种通用的字符集,可以表示世界上大多数语言的字符,包括中文。Python中,可以使用`str.decode()`函数进行解码,例如:`utf8_data....

Global site tag (gtag.js) - Google Analytics