第三章 深入分析Java Web中的中文编码问题
一、常见的编码格式
1.1 为啥要进行编码
- 在计算机中存储信息的最小单元是 1 个字节,即 8 个 bit, 所以能表示的字符范围是 0 ~ 255 个。
- 要表示的符号太多,无法用 1 个字节来完全表示。
1.2 编码
- ASCII 码
总共有 128 个,用 1 个字节的低 7 位表示, 0 ~ 31 是控制字符如换行、回车、删除等,32 ~ 126 是打印字符,可以通过键盘输入并且能够显示出来。 - ISO-8859-1
128 个字符显然是不够用的,所以 ISO 组织在 ASCII 的基础上扩展,他们是 ISO-8859-1 至 ISO-8859-15,前者涵盖大多数字符,应用最广。ISO-8859-1 仍是单字节编码,它总归能表示 256 个字符。 - UTF-16
它具体定义了 Unicode 字符在计算机中的存取方法。UTF-16 用两个字节来表示 Unicode 的转化格式,它采用定长的表示方法,即不论什么字符用两个字节表示。两个字节是 16 个 bit,所以叫 UTF-16。它表示字符非常方便,没两个字节表示一个字符,这就大大简化了字符串操作。 - UTF-8
虽说 UTF-16 统一采用两个字节表示一个字符很简单方便,但是很大一部分字符用一个字节就可以表示,如果用两个字节表示,存储空间放大了一倍,在网络带宽有限的情况下会增加网络传输的流量。UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度不同类型的字符可以由 1 ~ 6 个字节组成。
UTF-8 有以下编码规则:
如果是 1 个字节,最高位(第 8 位)为 0,则表示这是一个 ASCII 字符(00 ~ 7F)
如果是 1 个字节,以 11 开头,则连续的 1 的个数暗示这个字符的字节数
如果是 1 个字节,以 10 开头,表示它不是首字节,则需要向前查找才能得到当前字符的首字节
二、Java 中需要编码的场景
2.1 I/O 操作中存在的编码
我们知道涉及到编码的地方一般都在字符到字节或者字节到字符的转换上,而需要这种转换的场景主要是在 I/O 的时候,这个 I/O 包括磁盘 I/O 和网络 I/O,关于网络 I/O 部分在后面将主要以 Web 应用为例介绍。下图是 Java 中处理 I/O 问题的接口:
Figure xxx. Requires a heading
Reader 类是 Java 的 I/O 中读字符的父类,而 InputStream 类是读字节的父类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中处理读取字节到字符的转换,而具体字节到字符的解码实现它由 StreamDecoder 去实现,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。值得注意的是如果你没有指定 Charset,将使用本地环境中的默认字符集,例如在中文环境中将使用 GBK 编码。
写的情况也是类似,字符的父类是 Writer,字节的父类是 OutputStream,通过 OutputStreamWriter 转换字符到字节。如下图所示:
Figure xxx. Requires a heading
同样 StreamEncoder 类负责将字符编码成字节,编码格式和默认编码规则与解码是一致的。
如下面一段代码,实现了文件的读写功能:
String file = "c:/stream.txt";
String charset = "UTF-8";
// 写字符换转成字节流
FileOutputStream outputStream = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(
outputStream, charset);
try {
writer.write("这是要保存的中文字符");
} finally {
writer.close();
}
// 读取字节转换成字符
FileInputStream inputStream = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(
inputStream, charset);
StringBuffer buffer = new StringBuffer();
char[] buf = new char[64];
int count = 0;
try {
while ((count = reader.read(buf)) != -1) {
buffer.append(buffer, 0, count);
}
} finally {
reader.close();
}
在我们的应用程序中涉及到 I/O 操作时只要注意指定统一的编解码 Charset 字符集,一般不会出现乱码问题,有些应用程序如果不注意指定字符编码,中文环境中取操作系统默认编码,如果编解码都在中文环境中,通常也没问题,但是还是强烈的不建议使用操作系统的默认编码,因为这样,你的应用程序的编码格式就和运行环境绑定起来了,在跨环境下很可能出现乱码问题。
2.2 内存中操作中的编码
在 Java 开发中除了 I/O 涉及到编码外,最常用的应该就是在内存中进行字符到字节的数据类型的转换,Java 中用 String 表示字符串,所以 String 类就提供转换到字节的方法,也支持将字节转换为字符串的构造函数。如下代码示例:
String s = "这是一段中文字符串";
byte[] b = s.getBytes("UTF-8");
String n = new String(b,"UTF-8");
Charset 提供 encode 与 decode 分别对应 char[] 到 byte[] 的编码和 byte[] 到 char[] 的解码。如下代码所示:
Charset charset = Charset.forName("UTF-8");
ByteBuffer byteBuffer = charset.encode(string);
CharBuffer charBuffer = charset.decode(byteBuffer);