字节(byte)是数据传输和存储的最小单位。
字符编码
ASCII
在英文体系里面,所有可用的字符都可以用一个字节表示出来,即 ASCII码。
也就是说,一段字节如果编码方式是ASCII,那么一个字节就表示一个字符。
一个字节能表示 256 种状态,而 ASCII 只使用了 127 种。
ISO/IEC 8859-n
当计算机普及到了比如德国、法国和西班牙这些非英语语系的西方国家。虽然它们拥有自己的语言和特殊字符,但是其特殊字符数目和英文字符加起来还是不会超过 256 个,因此国际标准化组织为他们制定了一系列 8 位字符集编码表,即 ISO/IEC 8859-n 标准,例如 ISO/IEC 8859-1 就表示法语、芬兰语所用的西欧字符集。
中文编码
当计算机普及到了东亚国家,比如中国,单字节已经完全不能编码所有的象形文字了,此时一个字符(即一个汉字)就需要使用多个字节来编码,我国现行的汉字编码标准是 GB18030。每个汉字(字符)可以由1个、2个或者4个字节构成。
Unicode
在出现 Unicode 之前,每种文字都有自己独特的编码方式。同一段字节,在不同的编码方式下翻译出的字符序列基本上不一样。Unicode 要做的就是统一所有语言的编码方式,使用一套标准来编码地球上所有语言体系下的所有字符。
一开始,Unicode 使用两个字节来编码字符,这样最多支持 $2^{16}=65536$ 个字符。显然这是远远不能兼容地球上所有可能出现的语言字符的,比如汉字就不行。所以从第四版开始,Unicode 加入了 扩展字符集,使用4个字节来编码额外的字符,比如中国汉字。
中:4e2d 000d
国:000a 56fd
UTF-16
Unicode 只是一套全球化的编码规范,却不是计算机编码的具体实现方式。
计算机怎么使用 Unicode 编码还需要一些其他的技巧。
Java 默认使用 UTF-16 来编码解码字节流,它表示每两个字节表示一个常规字符,这样系统只需要老老实实的两个字节成对读取就能将字节流成功翻译为字符序列。
而扩展字符集中的字符,只需要用特殊的标志标记出来就行了。
这样做的一个明显的缺点就是,当文本中包含大量英文字符或者西欧字符时,统一用两个字节去编码和传输它们会浪费大量的流量,因为它们本来用一个字节就能表示出来。这样,我们需要考虑为不同的字符集使用不同数目的字节来编码,从而减小编码后的字节流的大小。
这就是 UTF-8 要做的事。
UTF-8
使用不同数目的字节来编码不同的字符集,例如 ASCII 字符集仍然使用 ASCII 编码,汉字等用 2 个或者更多的字节来编码。
这里 UTF-8 通过在字节的高位添加特殊的标志位来判断一个字符占用的字节数目:
- 以 0 开头的字节表示该字节就表示了一个 ASCII 字符;
- 以 11 开头的字节表示从当前字节往后共计 2 字节表示了一个字符,其他字节应以 10 开头;
- 以 111 开头的字节表示从当前字节往后共计 3 字节表示了一个字符,其他字节应以 10 开头;
- 以 1111 开头的字节表示从当前字节往后共计 4 字节表示了一个字符,其他字节应以 10 开头;
- ……
例如汉字“山”的 Unicode 编码是 5c71,即 0101 1100 0111 0001。该汉字编码需要两个字节,共 16 bits,由上表可知,在 UTF-8 规范下,应该使用三个字节来编码,并且三字节编码除去标志位刚好剩余 16 bits。
将 0101 1100 0111 0001 分为三段:0101, 110001, 110001,分别加上 UTF-8 的标志头:11100101 10110001 10110001。我们经常用两个16进制字符来表示一个字节,该编码转换成16进制为 E5B1B1。
所以汉字“山”用 UTF-8 编码成了 E5B1B1。
public static void main(String[] args) throws UnsupportedEncodingException {
String str = "山";
byte[] bs = str.getBytes("UTF-8");
System.out.println(String.format("%x", bs[0])); //=> e5
System.out.println(String.format("%x", bs[1])); //=> b1
System.out.println(String.format("%x", bs[2])); //=> b1
}
总之,数据存储和传输都是以字节为单位进行的,字符只不过是按照一定的映射关系,将一个或者多个字节翻译成人类语言中的对应符号。
java.io 流
Java 提供了两种方式来处理 流:
- 面向 字节流 的
InputStream
和OutputStream
- 面向 字符流 的
Reader
和Writer
字节流 是数据流动的基本单位,字符流 是对字节流进行特殊解码的结果。
Java 中负责将字节流解码成字符流的工具是 InputStreamReader
和 OutputStreamWriter
。
Unicode码元:将一个 Unicode 编码转换成一个 4 字节的 int 整数,所以它是 int 类型。
Reader
类声明了一个 read()
抽象方法:
abstract Reader {
abstract int read();
}
在上面的例子中,假如输入字节流的编码方法是 UTF-8,read()
方法读取一个字符,并返回该字符对应的 Unicode码元。具体过程是:
- 明确解码方式是 UTF-8
- 读到字节 11100101 时,明确该字节连同后面的两个字节表示一个字符
- 去掉字节串 11100101 10110001 10110001 中的标志位,得到该字符的 Unicode 编码 0101 1100 0111 0001
- 将该 Unicode 编码扩充到 4 字节:00000000 00000000 01011100 01110001
- 返回对应的 int 类型的整数 23,665
Writer
类的 write()
方法类似,输入一个 Unicode码元,将经过特定编码后的一个或多个字节返回到输出字节流中。
abstract class Writer {
abstract void write(int c);
}
从上图我们可以看到,将 字节输入流 转换为 字符输入流,或者将 字符输出流 转换为 字节输出流 的过程中,需要两个很关键的东西,一个是 Charset
,代表编码方式;一个是 StreamDecoder
和 StreamEncoder
,代表解码器和编码器。
FileInputReader
和 FileOutputWriter
分别是 InputStreamReader
和 OutputStreamWriter
的派生类。
内存对字符串的编码
字符串保存到内存中同样需要编码成一个一个的字节。
获取字符串在指定编码方法下的字节序列:
import java.io.UnsupportedEncodingException;
public class run {
public static void main(String[] args) throws UnsupportedEncodingException{
String str = "这是一段中文字符串";
byte[] bs = str.getBytes("UTF-8");
for(byte b : bs) {
String hexStr = String.format("%x ", b);
System.out.print(hexStr);
}
System.out.println();
}
}
// e8 bf 99 e6 98 af e4 b8 80 e6 ae b5 e4 b8 ad e6 96 87 e5 ad 97 e7 ac a6 e4 b8 b2
以指定编码方法从字节序列构建字符串:
import java.io.UnsupportedEncodingException;
public class run {
public static void main(String[] args) throws UnsupportedEncodingException{
String str = "中文字符串";
byte[] bs = str.getBytes("UTF-8");
String newStr = new String(bs, "UTF-8");
System.out.println(newStr);
}
}
评论区