(十四)UTF-32编码方式
以下摘自维基百科
UTF-32是32位Unicode转换格式(Unicode Transformation Formats, 或UTF)的缩写。UTF-32是一种用于编码Unicode的协定,该协定使用32位比特对每个Unicode码位进行编码(但前导比特数必须为零,故仅能表示21个Unicode码位)。与其他可变长度的Unicode转换格式(UTF)相比,UTF-32编码长度是固定的,UTF-32中的每个32位值代表一个Unicode码位,并且与该码位的数值完全一致。
UTF-32的主要优点是可以直接由Unicode码位来索引。在编码序列中查找第N个编码是一个常数时间操作。相比之下,其他可变长度编码需要进行循序访问操作才能在编码序列中找到第N个编码。这使得在计算机程序设计中,编码序列中的字符位置可以用一个整数来表示,整数加一即可得到下一个字符的位置,就和ASCII字符串一样简单。
但要注意的是程序想直接定位到第n个字符且不去考察前n-1个字符的情形其实很少见。也就是说,虽然UTF-32遍历字符时候每一个字符都可以方便地下标+1,但是实际上在UTF-8和UTF-16中可以用一个记录已经遍历过的总字符宽度加上当前字符的宽度的整数来代替,二者效率其实差不多。其他一些情形,例如打算直接读取第n个字符而不打算去考察之前的字符,例如一些哈希操作和高速搜索算法,实际上并不需要n非常精确。以截断为例,用UTF-8或UTF-16也很简单,此时只需要调整指针的位置到最接近的字符边界处即可,这是一个固定时间的操作。
UTF-32的主要缺点是每个码位使用四个字节,空间浪费较多。在大多数文本中,非基本多文种平面的字符非常罕见,这使得UTF-32所需空间接近UTF-16的两倍和UTF-8的四倍(具体取决于文本中ASCII字符的比例)。
尽管每一个码位使用固定长度的字节看似方便,但UTF-32并不如其它Unicode编码使用广泛。与UTF-8及UTF-16相比,UTF-32更容易遭到截断。即使使用了"定宽"字体,在大多数情况下用UTF-32计算显示字符串的宽度也并不比其他编码更加容易。主要原因是,存在着一个字符位置会有多于一种可能的码点(结合字符)或一个码点用多于一个字符位置(如CJK表意字符)。如果文本编辑器仅局限于从左到右且无结合字符,那么用UTF-32会有一定优势。但是这样的文本编辑器既然也不太可能支持非基本平面的字符,那么为什么不用UTF-16呢?
以下是UTF-32编码的例子:

需要注意的是BOM的情况也需要考虑进去,比如字符串ABC:

(十五)java的字符编码


下面主要讲解一下java是如何处理编码的问题的
几个基本概念

字符有三种形态:形状(显示在显示设备上)、数字(运行于JVM中,Java统一为unicode编码)和字节数组(不同的字符集有不同的映射方案)。
如此就可以明白四个重要的实体概念了(这四个概念来自于《Java NIO》一书):
字符集合(Character set):是一组形状的集合,例如所有汉字的集合,发明于公元前,发明者是仓颉。它体现了字符的“形状”,它与计算机、编码等无关。
编码字符集(Coded character set):是一组字符对应的编码(即数字),为字符集合中的每一个字符给予一个数字。例如最早的编码字符集ASCII,发明于1967年。再例如Java使用的unicode,发明于1994年(持续更新中)。由于编码字符集为每一个字符赋予一个数字,因此在java内部,字符可以认为就是一个16位的数字,因此以下方式都可以给字符赋值:
char c=‘中’
char c =0x4e2d
char c=20013
字符编码方案(Character-encoding schema):将字符编码(数字)映射到一个字节数组的方案,因为在磁盘里,所有信息都是以字节的方式存储的。因此Java的16位字符必须转换为一个字节数组才能够存储。例如UTF-8字符编码方案,它可以将一个字符转换为1、2、3或者4个字节。
一般认为,编码字符集和字符编码方案合起来被称之为字符集(Charset),这是一个术语,要和前面的字符集合(Character set)区分开。
转换的方式
- 从数字到形状—字体库
从JVM中的字符编码,到屏幕上显示的形状。这个转换是在字体库的帮助下完成的。例如windows默认的一些汉字字体,在Java中运行时是一个个的数字编码,例如0x4e2d,通过查找字体库,得到一个形状“中”,然后显示在屏幕上。
- 从数字到字节数组—编码
从JVM中的编码,到字节数组,这个转换被称之为编码。转换的目的是为了存储,或者发送信息。
同一个数字,例如0x4e2d,采用不同的字符集进行编码,能得到不同的字节数组。下面就java中需要编码的场景具体分析:
(1) I/O 操作
我们知道涉及到编码的地方一般都在字符到字节或者字节到字符的转换上,而需要这种转换的场景主要是在 I/O 的时候,这个 I/O 包括磁盘 I/O 和网络 I/O,关于网络 I/O 部分在后面将主要以 Web 应用为例介绍。下图是 Java 中处理 I/O 问题的读有关接口:

Reader 类是 Java 的 I/O 中读字符的父类,而 InputStream 类是读字节的父类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中处理读取字节到字符的转换,而具体字节到字符的解码实现它由 StreamDecoder 去实现,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。值得注意的是如果你没有指定 Charset,将使用本地环境中的默认字符集,例如在中文环境中将使用 GBK 编码。
写的情况也是类似,字符的父类是 Writer,字节的父类是 OutputStream,通过 OutputStreamWriter 转换字符到字节。如下图所示:

同样 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)内存中操作中的编码
在 Java 开发中除了 I/O 涉及到编码外,最常用的应该就是在内存中进行字符到字节的数据类型的转换,Java 中用 String 表示字符串,所以 String 类就提供转换到字节的方法,也支持将字节转换为字符串的构造函数。如下代码示例:
String s = "这是一段中文字符串";
byte[] b = s.getBytes("UTF-8");
String n = new String(b,"UTF-8");
另外一个是已经被被废弃的 ByteToCharConverter 和 CharToByteConverter 类,它们分别提供了 convertAll 方法可以实现 byte[] 和 char[] 的互转。如下代码所示:
ByteToCharConverter charConverter = ByteToCharConverter.getConverter("UTF-8");
char c[] = charConverter.convertAll(byteArray);
CharToByteConverter byteConverter = CharToByteConverter.getConverter("UTF-8");
byte[] b = byteConverter.convertAll(c);
Charset 提供 encode 与 decode 分别对应 char[] 到 byte[] 的编码和 byte[] 到 char[] 的解码。如下代码所示:
Charset charset = Charset.forName("UTF-8");
ByteBuffer byteBuffer = charset.encode(string);
CharBuffer charBuffer = charset.decode(byteBuffer);
这两个类已经被 Charset 类取代,编码与解码都在一个类中完成,通过 forName 设置编解码字符集,这样更容易统一编码格式,比 ByteToCharConverter 和 CharToByteConverter 类更方便。
Java 中还有一个 ByteBuffer 类,它提供一种 char 和 byte 之间的软转换,它们之间转换不需要编码与解码,只是把一个 16bit 的 char 格式,拆分成为 2 个 8bit 的 byte 表示,它们的实际值并没有被修改,仅仅是数据的类型做了转换。如下代码所以:
ByteBuffer heapByteBuffer = ByteBuffer.allocate(1024);
ByteBuffer byteBuffer = heapByteBuffer.putChar(c);
以上这些提供字符和字节之间的相互转换只要我们设置编解码格式统一一般都不会出现问题。
网友评论