先说编码(encode)
所谓的汉字编码,就是将一个个汉字,翻译成一个个字节(byte),进一步讲,是将一个字符串(String、char),转换成字节数组(byte[])。字节数组其实在各种编程语言中,用的地方还挺多,主要用在I/O方面。我们常说的二进制数据,在Java中,基本上可以等同于字节数组。所以,不论是序列化、文件操作、网络传输二进制流、输入输出二进制流,都会用到字节数组。
再通俗一点讲,汉字编码就是将一个汉字字符翻译成机器能识别的二进制数据。(非汉字同理)
主流的编码方式如下:
学过计算机的人都知道 ASCII 码,总共有 128 个,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126 是打印字符,可以通过键盘输入并且能够显示出来。
128 个字符显然是不够用的,于是 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,所有应用的最广泛。ISO-8859-1 仍然是单字节编码,它总共能表示 256 个字符。
它的全称是《信息交换用汉字编码字符集 基本集》,它是双字节编码,总的编码范围是 A1-F7,其中从 A1-A9 是符号区,总共包含 682 个符号,从 B0-F7 是汉字区,包含 6763 个汉字。
全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。
全称是《信息交换用汉字编码字符集》,是我国的强制标准,它可能是单字节、双字节或者四字节编码,它的编码与 GB2312 编码兼容,这个虽然是国家标准,但是实际应用系统中使用的并不广泛。
说到 UTF 必须要提到 Unicode(Universal Code 统一码),ISO 试图想创建一个全新的超语言字典,世界上所有的语言都可以通过这本字典来相互翻译。可想而知这个字典是多么的复杂,关于 Unicode 的详细规范可以参考相应文档。Unicode 是 Java 和 XML 的基础,下面详细介绍 Unicode 在计算机中的存储形式。
UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。(只能说绝大多数情况下是这样,后面有说明)
UTF-16 统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成。
UTF-8 有以下编码规则:
- 如果一个字节,最高位(第 8 位)为 0,表示这是一个 ASCII 字符(00 – 7F)。可见,所有 ASCII 编码已经是 UTF-8 了。
- 如果一个字节,以 11 开头,连续的 1 的个数暗示这个字符的字节数,例如:110xxxxx 代表它是双字节 UTF-8 字符的首字节。
- 如果一个字节,以 10 开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节
要了解Unicode就要了解UCS-2/UCS-4,还要了解BOM(Byte Order Mark)和BE/LE(Big Endian/Little Endian)。详细内容可读(http://blog.charlee.li/unicode-intro/)我这里只说一下结论吧:Unicode和UCS-2/UCS-4只是规定了字符用那些字节表示。UTF-8/UTF-16以及BOM才决定了字符在内存中真正的存储方式,真正的用什么样的字节存储,当然真正存储的时候,是需要根据UCS-2或UCS-4计算出的。
严格来说:UTF-16和UTF-8都是变长的,UTF-32才是定长的,UTF-16并不总是两个字节表示一个字符,UTF-16还可以表示UCS-4标准的字符,那个时候占用4字节,但是这一点基本上在开发中被忽略了,基本也可以被忽略。UTF-32之所以是4字节定长,是因为它完全用来表示UCS-4。
小插曲:四个“龍”字以田字形布局组成的一个汉字大家都没见过吧,但是确实在Unicode中有这个字,他用UTF-16存储就是4个字节。
我们常用的汉字,UTF-8是三字节,常用的英文字符,UTF-8是一字节。我们常用的汉字和英文字符,UTF-16都是双字节。所以UTF-8不一定比UTF-16省空间和流量,UTF-8比UTF-16更适合于网络传输的原因是,当丢失单个字节时,不会引起连锁的解码错误,而UTF-16丢失一个字节后,丢失位之后的所有字节解码都会出错。但是UTF-16占用空间更少,易于处理,所以UTF-16一般作为内存存储的编码方式。
再讲解码(decode)
解码是编码的反操作。如果说编码是将地球文字翻译成太阳文字,解码就是将太阳文字翻译成地球文字。
实际上,解码是将字节数组(byte[])按照源编码格式,翻译成汉字,或者翻译成另外一种编码格式的过程。
1、在编辑器中,如果你正在写一个.java文件,你点了保存或者Ctrl+S,这个内存中的java文件就要落地到磁盘上,这时候,系统必须要以一种编码格式对这个.java文件中的所有字符进行编码,才能翻译成二进制保存到磁盘上。
咱们编程都是英文编的,一般用任何编码方式都没什么问题。但如果你有一个变量String s=”一段中文”,这四个中文字符的存在,对文件编码格式提出了要求,可能会引起乱码。
默认情况下,保存是以操作系统的默认编码保存,一般中文操作系统是GBK。
(像notepad,可以保存成UTF-8,但是一定要保存成无BOM格式。)
理论上,java源文件,可以保存成任何编码格式,当然,如果有中文,这种格式必须支持中文,比如保存成GB2312\GBK\UTF-8\UTF-16都没有问题。
2、编译的时候。
用javac编译,是将.java文件的内容取出来,编译成.class文件。所以编译的时候,编译器得知道我之前.java文件是用那种编码方式进行的编码,如果不知道,取(解码)的时候,可就乱套了。我们可以运行一下javac -help命令,其中有一个参数-encoding就是干这件事的,官方解释是:指定源文件使用的字符编码。
如果不指定,则默认使用操作系统编码,一般中文操作系统是GBK。现在我们一般编程的时候,都会指定源码为UTF-8格式,用javac直接编译就会报错,错误提示一般是“编码GBK不可映射字符”或者“未结束的字符串面值”。那为什么我们一般很少遇到这种情况的,因为eclipse中,编译时的编码格式与文件格式一致,所以不会出现问题。这就是IDE为我们做的工作。
按照指定的源字符编码对二进制数据进行解码,这里解码后,不是用来输出,是马上用Unicode编码进行编码。Java编译的字节码文件,也就是.class文件中,所有的字符都是Unicode编码(实际是UTF-8)。这样,所有字节码文件就有了统一的,可支持全球语言的文件。
3、内存中
JVM加载.class文件的时候,就以Unicode编码方式加载就好了,汉字在内存里也是unicode方式存储的。这里实际是用UTF-16,估计是因为Java中的char数据类型都是双字节的吧。
实际上,char数据类型,在JVM中的标准就是UTF-16(无BOM)。String类型的实际存储是UTF-16BE。
4、使用时
无论我们是读取磁盘上的文件,还是读取网络传过来的数据流,我们必须知道他们正确的格式。并给我们的程序指定具体的解码类型。比如我有一个GB2312保存的本地文件,我用JAVA程序去读取里面的内容,起码我就得知道我应该用GB2312去解码,然后JVM会将解码后的字符串,转换成Unicode(UTF-16)。
当我们不知道源文件或源字符编码格式时,最好不要手动解码再编码,很有可能这个过程再也不可逆了。乱码了再也退不回的情况,我们在保存文件的时候经常遇到。(撤销操作另外,这里指再保存回原来格式的文本文件,乱码依然存在或者更严重)
5、JavaWeb开发时
说了这么多基础的知识,真到JavaWeb开发的时候,用到的可能性不大,除非是真的解决不了的编码问题,可以思考原理。
JavaWeb开发中一般需要注意的地方有这么几个:
- 所有的Java、JSP、XML等源文件都用UTF-8格式保存。
- XML文件头行设置<?xml version=”1.0″ encoding=”UTF-8″>
- JSP设置编码格式<%@page contentType=”text\html; charset=UTF-8″%>
- HTML中加入<meta HTTP-equiv=”Content-Type” content=”text/html; charset=UTF-8″/>
- 用框架的话设置框架的编码方式为UTF-8
- 获取表单参数之前调用request.setCharacterEncoding(“UTF-8”)
- 设置中间件(如Tomcat)Connector的URIEncoding=”UTF-8″
一般设置前四项都不会有问题。
参考:
http://lukejin.iteye.com/blog/586088
http://www.zhihu.com/question/30945431/answer/50046808?group_id=590118722086309888
http://www.zhihu.com/question/23374078
http://blog.csdn.net/scyatcs/article/details/31356823
http://cnn237111.blog.51cto.com/2359144/1080628
http://blog.sina.com.cn/s/blog_63597c6901012oyf.html
http://stackoverflow.com/questions/2164804/from-compilation-to-runtime-how-does-java-string-encoding-really-work
https://zh.wikipedia.org/wiki/UTF-16
https://zh.wikipedia.org/wiki/UTF-8
http://blog.charlee.li/unicode-intro/