字符编码

引言: 字符编码

字符编码

在早期的计算机系统中,为了给字符编码,美国国家标准学会(American National Standard Institute:ANSI)制定了一套英文字母、数字和常用符号的编码,它占用一个字节,编码范围从0到127,最高位始终为0,称为ASCII编码。

例如,字符A的编码是0x41,字符1的编码是0x31

ASCII是能表示英文了,但是中文、日文、韩文怎么办?


但是,如果要把汉字也纳入计算机编码,很显然一个字节是不够的。各个国家都有了自己的标准。

GB2312标准使用两个字节表示一个汉字,其中第一个字节的最高位始终为1,以便和ASCII编码区分开。例如,汉字GB2312编码是0xd6d0

类似的,日文有Shift_JIS编码,韩文有EUC-KR编码,这些编码因为标准不统一,同时使用,就会产生冲突。

那各个国家交流不就乱套了吗


为了统一全球所有语言的编码,全球统一码联盟发布了Unicode编码,它把世界上主要语言都纳入同一个编码,这样,中文、日文、韩文和其他语言就不会冲突。

Unicode编码需要两个或者更多字节表示,我们可以比较中英文字符在ASCII、GB2312和Unicode的编码:

英文字符’A’的ASCII编码和Unicode编码:

1
2
3
4
5
6
         ┌────┐
ASCII: │ 41 │
└────┘
┌────┬────┐
Unicode: │ 00│ 41 |
└────┴────┘

英文字符的Unicode编码就是简单地在前面添加一个00字节。

中文字符’中’的GB2312编码和Unicode编码:

1
2
3
4
5
6
         ┌────┬────┐
GB2312: │ d6 │ d0 │
└────┴────┘
┌────┬────┐
Unicode: │ 4e │ 2d │
└────┴────┘

那我们经常使用的UTF-8又是什么编码呢?

因为英文字符的Unicode编码高字节总是00,包含大量英文的文本会浪费空间,所以,出现了UTF-8编码,

它是一种变长编码,用来把固定长度的Unicode编码变成1~4字节的变长编码。

通过UTF-8编码,英文字符A的UTF-8编码变为0x41,正好和ASCII码一致,而中文’中’的UTF-8编码为3字节0xe4b8ad

UTF-8编码的另一个好处是容错能力强。如果传输过程中某些字符出错,不会影响后续字符,因为UTF-8编码依靠高字节位来确定一个字符究竟是几个字节,它经常用来作为传输编码。

Java和编码

在Java中,char类型实际上就是两个字节的Unicode编码。如果我们要手动把字符串转换成其他编码,可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
byte[] b1 = "Hello中".getBytes(); // 按系统默认编码转换,不推荐
/* 查看系统默认的编码
System.out.println(Charset.defaultCharset());
*/
byte[] b2 = "Hello中".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b3 = "Hello中".getBytes("GBK"); // 按GBK编码转换
byte[] b4 = "Hello中".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换

System.out.println(Arrays.toString(b1));// 按系统默认编码而定
System.out.println(Arrays.toString(b2));//[72, 101, 108, 108, 111, -28, -72, -83]
System.out.println(Arrays.toString(b3));//[72, 101, 108, 108, 111, -42, -48]
System.out.println(Arrays.toString(b4));//[72, 101, 108, 108, 111, -28, -72, -83]

如果要把已知编码的byte[]转换为String,可以这样做:

1
2
String s1 = new String(arr, "GBK"); // 按GBK转换
String s2 = new String(arr, StandardCharsets.UTF_8); // 按UTF-8转换

始终牢记:Java的Stringchar在内存中总是以Unicode编码表示。

String类的演变

对于不同版本的JDK,String类在内存中有不同的优化方式。

具体来说,早期JDK版本的String总是以char[]存储,它的定义如下:

1
2
3
4
5
public final class String {
private final char[] value;
private final int offset;
private final int count;
}

而较新的JDK版本的String则以byte[]存储:如果String仅包含ASCII字符,则每个byte存储一个字符,否则,每两个byte存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String通常仅包含ASCII字符:

1
2
3
4
public final class String {
private final byte[] value;
private final byte coder; // 0 = LATIN1, 1 = UTF16
}

对于使用者来说,String内部的优化不影响任何已有代码,因为它的public方法签名是不变的。

小结

  • Java内存中使用Unicode表示charString
  • 转换编码就是将Stringbyte[]转换,需要指定编码
  • 转换为byte[]时,始终优先考虑UTF-8编码。

改编自廖雪峰的官方网站,原文链接