前言
在总结 Java 流相关的技术时,有必要简单回顾一下字符集相关的内容。
Unicode
为什么要编码
要回答这个问题必须回答计算机是如何表示我们人类能够理解的符号的,这些符号也就是我们人类使用的语言。由于人类使用的语言太多,这些语言的符号太多,无法用计算机中一个基本的存储单元——字节(byte)来表示,因此我们需要有字节与字符之间相互转换的过程,即编码和解码的过程。
总结一下,编码的原因就是如下几条:
- 计算机的存储信息的最小单元是一个字节,8bit,存储信息的范围是0-255
- 人类要表示的字符太多,一个字节表示不过来。
简介
Unicode 是一种编码方案,其解决的问题是早期 ASCII 等其他编码无法统一全球文字编码的问题。Unicode 把所有的语言都统一到一套编码里,将世界上各种文字的每一个字符指定唯一编码,实现跨语言,跨平台的应用,简单理解 Unicode 就是世界上所有字符的一直符号集。
Unicode的问题
由于 Unicode 只是字符和二进制值的映射,它并没有指定每个字符如何转换成字节的表示方法,如汉字“严”对应的 unicode 十六进制数为4E25,转换成二进制需要15位,意味着至少要有2个字节才能表示这个字符以让计算机进行处理。表示其他更大的字符,则需要3个或者4个字节。
此时就带来了两个问题:
- 计算机如何区分 Unicode 编码和 ASCII 编码方式:即计算机如何判断三个字节表示的是一个字符还是三个字符。
- 如果 unicode 统一规定每个字符用三个字节来表示,那么对于英文字母和 基本汉字而言,前两个字节都是无用的,这会浪费大量内存。
以上问题造成的结果为:
- 出现了 Unicode 的多种存储方式,即有许多种不同的二进制格式可以用来表示 unicode
- Unicode 在很长一段时间内无法推广,直到互联网的出现
UTF-8
随着互联网的兴起,推动了统一编码方式的进程。最终UTF-8称为了互联网上使用最广的一种 Unicode 的实现方式,其他的编码方式还有 UTF-16,UTF-32等。
UTF-8最大的特点即,它是一种变长的编码方式,它可以使用1~4字节来表示一个符号。这种方式可以有效地节省内存空间。
UTF-8与Unicode的转化关系:
Unicode编码(十六进制) | UTF-8 字节流(二进制) |
---|---|
000000-00007F | 0xxxxxxx |
000080-0007FF | 110xxxxx 10xxxxxx |
000800-00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000-10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
码点与代码单元
码点(code point):是指与一个编码表中的某个字符对应的代码值,在 Unicode 标准中,码点采用十六进制书写,并加上 U+ 前缀,
代码级别(code plane):Unicode 的码点可以分为17 个代码级别,第一个代码级别称为基本的多语言级别(basic multilingual plane),码点从U+0000 到 U+FFFF,其中包括经典的 Unicode 代码,其余的16个级别码点从 U+10000 到 U+10FFFF,其中包括一些辅助字符(supplementary character)
代码单元(code unit):在基本的多语言级别中,每个字符用16位表示,称为代码单元,而辅助字符采用一对连续的代码单元,他们存在于**替代区域(surrogate area)**中。
在 Java 中, char 类型占两个字节,描述了UTF-16 编码中的一个代码单元,作者不建议使用 char类型,除非要处理代码单元。
Java 中常用的关于码点与代码单元的操作:
String hello = "hi𝕆";
// 返回代码单元的数量 4
int length = hello.length();
for (int i = 0; i < length; i++) {
// 获取代码单元
char charAt = hello.charAt(i);
System.out.println(Integer.toHexString(charAt));
}
System.out.println("-======================");
// 返回实际的长度,即码点的数量
int codePointCount = hello.codePointCount(0, length);
// 获取第 i 个码点
// int index = hello.offsetByCodePoints(0, i);
// int charAt = hello.codePointAt(index);
// 变量字符串,获取每一个码点
for (int i = 0; i < codePointCount;) {
int cp = hello.codePointAt(i);
if (Character.isSupplementaryCodePoint(cp)) {
i += 2;
} else {
i++;
}
System.out.println(Integer.toHexString(cp));
}
// 可以使用下来语句实现回退操作
// i--;
// if (Character.isSurrogate(hello.charAt(i))) i--;
// int cp = hello.codePointAt(i);
// 字符串转化为码点数组-- int类型数组,每个元素代表一个码点
int[] codePoints = hello.codePoints().toArray();
// 码点数组转为字符串
String str = new String(codePoints, 0, codePoints.length);
流
简介
在 Java API 中,可以从其中读入一个字节序列的对象称作输入流,而可以从其中写入一个字节序列的对象称为输出流。这些字节序列的来源地和目的地可以是文件,而且通常都是文件,也可以是网络传输,甚至是内存块。抽象类 InputStream 和 OutputStream 构成了 输入输出类的层次结构的基础。
因为面向字节的流不便于处理已 Unicode 形式存储的信息(Unicode 中每个字符都使用了多个字节来表示),所以抽象类 Reader 和 Writer 中继承出来了一个专门用于处理 Unicode 字符的单独的类层次结构。这些类拥有的读入和写出操作都是基于两字节的 Unicode 码元的,而不是基于单字节的字符。
read 和 write 方法在执行时都会被阻塞,直至字节确实被写入或写出,这意味着如果流不能被立即访问(通常因为网络链接忙),那么当前线程被阻塞。这使得在这两个方法等待指定的流变为可用的这段时间里,其他线程就有机会去执行由于的工作。
关闭一个流的同时还会flush该输出流的缓冲区,所有被临时置于缓冲区中,以便用更大的包的形式传递的字符在关闭输出流时都将被送出。
利用 idea 的diagram 工具可以很方便地绘出类图,我们来看下主要 IO 类的类图
组合流过滤器
Java 的基础类库设计者使用了 装饰者模式 来设计整个 IO 流源码,好处是使得每个流都职责明确:如FileInputStream 用来读取文件, 而 DataInputStream 用来读取数值,利用装饰者模式可以很好地将各种流拼接在一起以获得自己想要的流,如以下代码,使用缓存流从文件中读取数字。
DataInputStream din = new DataInputStream(
new BufferedInputStream(
new FileInputStream("1.txt")));
以及从一个 ZIP 文件中通过一下流序列来读取数字:
ZipInputStream zin = new ZipInputStream(new FileInputStream("1.zip"));
DataInputStream din = new DataInputStream(zin);
文本输入与输出
在保存数据时,可以选择二进制格式或者文本格式。例如,整数1234存储为二进制数时,它被写为由字节00 00 04 D2
构成的序列(十六进制表示),而存储成文本格式时,它被存为字符串"1234"
。我们先讨论利于阅读的文本格式。
在存储为文本格式时,需要考虑字符编码(character encoding) 方式,OutputStreamWriter
将使用选定的字符编码方式,把 Unicode 字符流转化为字节流。而 InputStreamReader
将包含字节(用某种字符编码方式表示的字符)的输入流转为可以生成 Unicode 码元的读入器。
文本输出
文本输出一般使用 PrintWriter
PrintWriter out = new PrintWriter("employee.txt");
out.print("Harry Potter");
out.print(" ");
out.println(1500);
out.flush();
读入文本输入
众所周知
- 二进制格式写出数据: 使用
DataOutPutStream
- 二进制格式读入数据:使用
DataInputStream
- 文本格式写出数据:使用
PrintWriter
在 JDK5 之前,处理文本输入的唯一方式为 BufferedReader
类
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("employee.txt"), "UTF-8"));
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
字符集
在早期的 Java 版本中,Java 采用了16位的 Unicode 字符集,因此在 Java 中, char 类型描述了 UTF-16 编码中的一个代码单元,一个代码单元由2个字节组成。在 JDK 1.4 中引入的 java.nio 包中的 Charset 类统一了对字符的转换。
获取 Charset 方法:
Charset cset = StandardCharsets.UTF_8;
Charset.forName("UTF-8");
查看当前JDK中支持的所有字符集:
Charset.availableCharsets();
使用字符集进行编码和解码
// encode
Charset cset = StandardCharsets.UTF_8;
String hello = "hello";
ByteBuffer buffer = cset.encode(hello);
byte[] bytes = buffer.array();
// decode
byte[] bytes = buffer.array();
ByteBuffer bbuf = ByteBuffer.wrap(bytes, 0, bytes.length);
CharBuffer cbuf = cset.decode(bbuf);
String str = cbuf.toString();
注意:根据使用不同的处理器类型,在内存存储整数和浮点数时有两种不同的方法:高位在前顺序(MSB)与低位在前顺序(LSB),这就使得在存储C和C++文件时数据从一个平台迁移到另一个平台也是一种挑战。在Java中,所有的值都按照高位在前的模式(MSB)写出,不管使用何种处理器,这使得Java数据文件可以独立于平台。
读出二进制文件
DataOutput
和 DataInput
接口定义了一系列用于以二进制格式读写数组、字符、boolean值和字符串的方法。
RandomAccessFile
类可以在文件的任何位置查找和写入数据,磁盘文件都是随机访问的,但是从网络来的数据流却不是。
JAR
文件只是带有一个特殊项的 ZIP 文件,这个项称作清单,可以调用 JarInputStream
和 JarOutputStream
来读写清单项。
Reference
- Java中弄懂Unicode和UTF-8编码方式
- Java中的代码点和代码单元
- 《Java核心技术卷二 第九版》