java字符集
我们在java文件中写的代码的所有字符都是字符串。例如:
1 | int ia = 123; |
上面的代码,使用二进制查看如下:
1 | 00000000: 696e 7420 6961 203d 2031 3233 3b0a 5374 int ia = 123;.St |
可以看到,空格: 0x20, 分号: 0x3b, 换行:0x0a, 引号:0x22
其实对于,计算机来说,它根本就不知道,任何所谓,字符串,汉字,英文字母,数字等等。其实在计算机上面所有我们所能看到的所谓汉字,英文字母,数字都是图片(可以简单认为是图片,或者说是 图形 graph)。
那么,如何存储这些图形呢? 例如:在 Hello.java 文件中有这样一句代码片断:
1 | String str = new String("你好"); |
现在,已经知道 String
其实是6个图形。那么 Hello.java 文件中是要存储这 6 个图形数据吗?图形数据如何表示呢?由谁来定 S 个图形的数据具体是什么呢? 假如由当前的使用的编辑器来定,则 上面的代码是在 eclipse 中编辑的,所以对于 eclipse 它可以定 S 这个字符的图形数据。eclipse 知道如何解析,并显示这个字符。但是,问题来了,我要使用 notepad.exe 来查看 Hello.java 文件,但是对于 notepad 来说,它并不清楚 eclipse 到底是存储的 S 这个图形的数据结构。则 notepad 肯定是无法正常显示出 S 这个字符了。
但是,事实上,使用 eclipse 编写的代码,notepad 等,任何文本编辑器都可以正常打开。并显示出 String 这些字符。这是怎么回事呢?难道所有文本编辑器都相互将如何保存和显示文本的格式都公开了,当 notepad 打开 eclipse 编辑的 Hello.java 时,就使用 eclipse 的图形解析程序,将 Hello.java 中存储的图形数据。进行解析展示。
但是,如果是这样,notepad 就需要知道 Hello.java 这个文件是 eclipse 编辑的,而不是 notepad++ 或者 ue 等编辑器编辑的。所以 对于每一个文本编辑器,都必须要在编辑的文件中标记清楚,这个文件是由什么工具编辑的。
显然,对于字符的保存和显示所以按照上面的思路去实现,每个文本编辑器要打开显示一个文件将变得非常复杂。
上面的思路是:Hello.java 中就存储,字符的图形数据,这个图形数据可能非常复杂。不同的字符,要表达清楚这个字符,可能图形数据完全不同,如果按照这个思路实现,文本存储,将使得文本的显示变得异常复杂(需要解析每一个图形数据)。并且,常用的字符,就那么多。例如一本 100 万字的小说,其中常用字可能就只有 2000 个,所以,如果使用上面的实现,则同一个字符,将要在这个小说文件中存储 1000000 / 2000 = 500 个完全相同的字符数据。例如 “我” 这个常用字,其图形数据需要 20 字节(假设的,真实需要描述这个字符肯定比较复杂)。这样的话,20 * 500 = 10000, 大约需要 10kb. 而对于每一个字符,其图形数据肯定是完全一样的,那么每出现一次,就存储一次,完全是没有必要的,这样做非常浪费存储空间。
考虑下面的思路:给每一个字符编号,每一个字符对应惟一的一个编号,然后在文件中存储这个编号就可以了。然后,另外创建一个文件 CharsetMapFile(字符编号映射文件),这个文件中存储所有的字符的图形数据和编号。当向一个文本文件中写入一个字符时,就将其编号保存到这个文件中。
当 notepad 打开这个文件时,所读到的全部是字符编号,就到 CharsetMapFile 找到,编号对应的图形数据,然后,就可以正常显示出文件了。
这样做的,图形数据只需要在 CharsetMapFile 存储一份就可以了。所有的文本文件都存储字符编号。
假设,使用 2 个字节进行编号,则一共可以有 2 ^ ( 2 * 8 ) = 2 ^ 16 = 65536 个编号。所以可以有 65536 个字符,可以使用 2 个字节来表示。 常用汉字,英文字母,数字,标点符号也就 3,4千个字符。可见使用 2 个字节来对字符进行编号完全可行。
上面的假设成为了现实,CharsetMapFile 其实就是所谓 字符集(Charset)。字符集就是对字符进行编号。所谓不同的字符集其实就是相同的字符由于编码规则不同,从而造成同一个字符其所对应的编号不同,则这两种编码规则,将产生不同的字符集。
例如: UNICODE 字符集,其中按字形顺序进行编码:
1 | 仨: 0x4EE8 |
这些字符都是 亻偏旁,进行编码。
对于 gb2312 使用将汉字,按照拼音进行排列,然后,开始逐个编码。
1 | 鞍:0xB0B0 (0x978D) |
这些字符都是按拼音 an 来进行编码。其中括号内的是该字符的 unicode 编码。
有了字符集的概念,就可以重新考虑,Hello.java 如何存储了。
1 | // 使用 unicode 表示 |
上面的代码中的每一个字符都有一个2个字节的unicde码。那么,文件就可以直接将这些 unicode 代码保存起来。
1 | S:0x0053 t:0x0074 r:0x0072 i:0x0069 n:0x006e g:0x0067 (空格):0x0020 |
String str = new String("你好");
30个字符,所以,上面使用 unicode 编码存储,一共是 30 * 2 = 60 个字节。
但是,实际 Hello.java 中,存储却不是这样。Hello.java 文件使用的文件编码是 utf-8, 这是一种对 unicode 编码的线性映射编码。可以说 unicode 是一种编码规范,这种规范,将所有的字符进行了编码,分配了惟一的 UNICODE。 但是呢,由于存储空间的问题,例如,一部 100 万字的英文小说,如果使用 unicode 编码来存储,将浪费一半的空间。因为,英文字母和常用的标点符号的 unicode 编码中,前一个字节,总是 0x00,所以可以考虑,将其去掉,这样,存储空间将节省一半。所以就出现了,所谓 utf-8 编码,其实 utf-8,使用的是 unicode 的编码,然后,对编码进行了线性变换,这种线性变换后的存储将更加适合网络传输,更加安全。所以 utf-8 算是对 unicode 的一种具体实现。
其实,我们上面设计的 unicode 存储,就是 UTF-16BE (unicode 大端存储)存储方案。当我们,使用 windows 自带的 记事本(notepad.exe) 程序,保存 String str = new String("你好");
然后选择注意在保存时选择,UNICODE 格式。将保存成上面 unicode 格式的数据。
notepad 支持 4 种编码格式:
ANSI: 和系统设置的区域有关,简体中文环境下 ANSI 就是 GBK 编码
Unicode: UTF-16LE unicode 小端
Unicode big endian: UTF-16BE unicode 大端
UTF-8: utf-8 编码
UTF-16LE 和 UTF-16BE 完全使用字符的 unicode 码,区别就是表示字符的 2 个字节的高位字节和底位字节的顺序。高位字节先存储为 big endian, 低位字节先存储为 little endian. 例如:’你’ unicode 编码是: 0x4F60, 其高位字节是: 0x4F, 低位字节是:0x60, 则 0x4F60 就是大端存储,而 0x604F, 则是小端存储。
对于 java 语言来说,其 数值和字符串 都使用 Big Endian 的存储方式。
String 类的几个方法:
1 | // 返回指定索引位置的字符。 |
java 中的 char 表示的就是字符,’a’ , ‘你’, ‘!’, ‘界’ ,都是字符。那么,char 如何存储呢。 char == 字符,由上面的分析,可以字符可以由,字符集中的编码来表示,例如: ‘你’: unicode: 0x4F60 gbk: 0xC4E3 。不同的字符集有不同的编码。在 java 在使用 Unicode big endian 来表示一个字符 <==> UTF-16 编码,也就是直接使用字符的 unicode 编码来代表一个字符。
也就说,在代码中:
1 | // \u0030 表示 unicode 字符(char) 0, |
从变量的命名,也可以看到,java 最终将这些字面量(变量名)和 字符串,都以 UTF-16 编码方式存储。
所以,我们甚至可以完全使用 unicode 字符来编码:
1 | // 注意下面的代码并不是乱码,只是使用 unicode 字符来编码 |
同时,对于字符串来说:
String a = “你好world世界”;
<==>
a = new String({‘你’, ‘好’, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’, ‘世’, ‘界’});
<==>
a = new String({‘\u4f60’, ‘\u597d’, ‘\u0077’, ‘o’, ‘r’, ‘l’, ‘d’, ‘世’, ‘界’});
1 | String ss = "你好"; |
Charset.defaultCharset() 方法可以获得默认字符集。
1 | // String.getChars |
源文件格式编码与 class 文件 编码
对于 java 源文件来说,可以使用不同类型的编码,例如 gbk, utf-8, 自然,其中的出现的字符串,也是 gbk ,utf-8编码。但是经过 javac 编译之后 生成的class 文件却严格使用 utf-16 编码,所以不同类型的文件,经过编码之后,其编码将发生转换。
but,问题来了, 当我们在 windows 命令行下面调用 javac 执行编译的时候,javac 如何知道当前 Hello.java 文件的编码格式呢? 对于文本文件 Hello.java 来说,其文件中并没有关于,文件是什么编码格式的说明。所以,必须从外部,获得文件格式。
javac 将使用当前系统环境的默认编码,ANSI,在 windows 平台上 ANSI 映射为 bgk 字符集。所以如果 Hello.java 使用的是 gbk 字符集,那么下面的编译过程将不会出现问题。
1 | // Hello.java |
其实,上面使用 javac 编码 utf-8 源文件时,之所以出现问题,就是 javac 采用了默认的字符编码。 查看 javac 帮助,可知 javac 支持一个参数: encoding
1 | // 直接在命令行 输入 javac , 然后 回车 |
所以,如果 代码源文件不同平台默认的字符集,则需要手动指定其编码即可,正常编码了。
1 | // 还是上面的保存成 utf-8 格式的 Hello.java 文件 |
类似地,可以将 Hello.java 使用 记事本,保存成 utf-16le 和 utf-16be 格式,然后
使用 javac 编译,同样也是编译不通过,所以需要显示指定,文件编码,
1 | // utf-16le 和 utf-16be 编码的 Hello.java 文件 |
如何查看当前系统所支持的所有字符集?
1 | SortedMap<String,Charset> characters = Charset.availableCharsets(); |
Character To Glyph Index Mapping Table 这篇文章中有讲到,如何将 character 映射到 字体文件的的某个具体的字符上。
JDK 对各种字符集的支持,以及编码和解码
jdk提供了三个类用来处理各种字符集。
字符集类 Charset
java.nio.charset.Charset
A named mapping between sequences of sixteen-bit Unicode code units and sequences of bytes. This class defines methods for creating decoders and encoders and for retrieving the various names associated with a charset.
字符编码类 CharsetEncoder
java.nio.charset.CharsetEncoder
An engine that can transform a sequence of sixteen-bit Unicode characters into a sequence of bytes in a specific charset.
字符解码类 CharsetDecoder
java.nio.charset.CharsetDecoder
An engine that can transform a sequence of bytes in a specific charset into a sequence of sixteen-bit Unicode characters.
这三个类都是抽象类,是各种具体的字符集的实现的共同基类。具体的字符集,需要继承 Charset 类,并实现下面两个方法。
1 | public abstract CharsetDecoder newDecoder(); |
newDecoder 获得具体实现字符集的解码类。
newEncoder 获得具体实现字符含棉编码类。
各种字符集的实现在:
sun.nio.cs 和 sun.nio.cs.ext 包中
sun.nio.cs 在 jre\lib\rt.jar
sun.nio.cs.ext 在 jre\lib\charsets.jar
sun.nio.cs 包中有 UTF-8, UTF-16, UTF-16LE, UTF-16BE 等字符集的实现。
sun.nio.cs.ext包中有 GBK, BIG5 字符集的实现。
CharsetProvider
java.nio.charset.spi.CharsetProvider 提供了 charsetForName 方法用来查找上面的具体字符集的实现。
jvm 的默认字符集
使用 System.getProperty(“file.encoding”) 可以获得 JVM 的默认字符集的名称。对于输入,输出字符流,如果不提供字符集参数,则使用这个默认字符集。
同时 jvm 提供的 Charset.defaultCharset() 方法返回 jvm 默认的 Charset 对象(字符集对象),这个默认的 Charset 对象,就是使用 file.encoding 作为默认的字符集名称创建的。
所以不,file.encoding 设置成 gbk 时, Charset.defaultCharset() 将返回以 gbk 字符集。
默认字符集的作用,就是在涉及到 char 转 byte 的API,如果没有提供字符集,没有提供字符集,则将使用这个默认的字符进行 char 转 byte 的操作。涉及到 char 转 byte 的 API 有:String.getBytes, 还有常用的 System.out.println 方法的类 PrintStream,这个类,如果指定字符集,则 All characters printed by a PrintStream are converted into bytes using the platform’s default character encoding.
还有 FileWriter 类直接使用默认的字符集对字符进行编码:
Convenience class for writing character files. The constructors of this class assume that the default character encoding and the default byte-buffer size are acceptable. To specify these values yourself, construct an OutputStreamWriter on a FileOutputStream.
1 | // 假设,启动 jvm 的时候,传递了参数 |