不少人常常分不清UTF-8编码和UTF-16编码,或常常会问"Unicode编码和UTF-8编码有什么区别联系","Java的外码内码又是什么东西",这篇文章主要作一个关于编码知识的简单扫盲,包括对一些常见概念混淆进行区分讲解。java
咱们常常提到的关于编码的概念能够粗略划分为两类:数组
Unicode字符集一开始提出的时候,认为码值范围为0-65535(0-FFFF,这一段区域也被称为Basic Multilingual Plane, 简称BMP)就能够表示全部的字符,但随着时代发展,0-FFFF也不够容纳全部字符,所以Unicode划出了一个代理区:D800-DFFF, Unicode标准规定U+D800 - U+DFFF的值不对应于任何字符。这也是为何有些人说:有些字符须要用两个Unicode字符去表示的缘由。函数
目前Unicode的编码空间为0-10FFFF,根据第一段落能够得知,当某个字符的Unicode码值落在0-FFFF时,则只用一个Unicode字符便可表示,不然就会用两个。编码
Utf-8全称为8-bit Unicode Transformation Format,是一种针对Unicode字符集的可变长编码,不一样的Unicode码点会使用不一样的字节数去存储,如ascii码(都小于128)则会使用1个字节去存储,一些经常使用字符(如部分中文)会使用2~3个字节去存储,这有一些优点,首先对于ascii码彻底兼容,且对于某些场景(只存在ascii码)编码后占用空间少,缺点也很明显,当遇到的都是须要占用3个字节存储的Unicode码点时,则会耗费更大的空间。代理
utf-8编码的基本规则以下:code
Unicode码范围 | Utf-8编码格式 |
---|---|
0x0000-0x007F(0~127) | 0xxxxxxx |
0x0080-0x07FF(128~2047) | 110xxxxx 10xxxxxx |
0x0800-0xFFFF(2048-65535) | 1110xxxx 10xxxxxx 10xxxxxx |
0x10000-0X10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
首先将Unicode码用二进制表示,而后根据其所属的Unicode范围对应的Utf-8编码格式截取最后几位。orm
如0x0000-0x00FF对应的Utf-8编码格式是0xxxxxxx,则截取最后7位(有效表示位其实也只有7位,由于从右往左第8位必定是0),对应填到x中。对象
同理,0x0080-0x07FF(0000 0000 1000 0000 — 0000 0111 1111 1111),则截取后11位(有效表示位其实只有11位,由于从右往左第12位必定是0),对应填到x中。utf-8
0x0080-0xFFFF(0000 1000 0000 0000 — 1111 1111 1111 1111),截取后15位(有效表示位其实只有16位,由于从右往左第17位必定是0),对应填到x中。ci
0x100000-0X10FFFF(0000 0001 0000 0000 0000 — 0001 0000 1111 1111 1111 1111),截取后21位(有效表示位其实只有21位,从右往左第22位必定是0),对应填到x中。
所以,咱们能够获得全部Unicode的Utf-8编码规则。
在Unicode字符集中讲到,Unicode字符集存在一个拓展区域:D800-DFFF,用于表示码点在0x10000-0x10FFFF范围的字符。当碰到某个在该范围内的Utf-16字符,须要再读一个Utf-16字符,将两个Utf-16字符组合表示一个Unicode字符。
Unicode码范围 | Utf-16编码格式 |
---|---|
0x0000-0xFFFF(0~65535) | 使用2个字节存储 |
0x10000-0x10FFFF(65536~) | 使用4个字节存储,须要利用上述提到的代理区 |
接下来将Unicode码用二进制表示,尝试将它用Utf-16编码格式进行编码。
对应0x0000-0xFFFF范围的Unicode码,直接将这16为对应填入两个字节(刚好16位)就能够获得Utf-16编码。
而对应0x10000-0x10FFFF的Unicode码,须要有一些特殊处理:
经过处理,前导代理和后导代理刚好占满了0xD800—0xDFFF这一段代理区域,这样处理的一个优势在于,看到每个Utf-16编码,能够很清楚地肯定它是属于前导代理、后导代理仍是除此之外的BMP区域中的Unicode。
MUTF-8(Modified UTF-8)编码,能够认为是对UTF-16编码的再编码。它的编码方式与UTF-8编码很是类似,只须要记住某些不一样的状况,其余都与UTF-8编码一致。
具体的不一样状况有二:
因此网上常常会提到UTF-8编码,又提到用1—6个字节去编码,其实说的是MUTF-8编码。
Java的内码是UTF-16,外码是MUTF-8。那什么是内码和外码呢?
内码:程序内部使用的字符编码,如java的char,因此java的char是2字节16位;
外码:程序外部交互时使用的字符编码,如class文件。
在深刻理解Java虚拟机第三版6.3.2节中,咱们能够得知其实Java的字符串常量(如String str="hello world")都是以CONSTANT_Utf8_info类型存在常量池中的,class文件的编码是MUTF-8,因此CONSTANT_Utf8_info中存储的根据不一样的实现通常是存储MUTF-8字节数组或UTF-16字符数组,每次构建时java.lang.String对象时,须要经过MUTF-8=>UTF-16的一个编码转换将外码转为内码,再将其塞到char数组(value)中。
在Java API层对字符串的操做,其实通常也是对UTF-16字符的操做,如charAt函数:
public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; }
charAt函数实际是返回了一个char,因此是返回了一个UTF-16字符,它不必定是一个完整的Unicode码点。
固然,在Java API层也可使用getBytes(”UTF-8")则是返回UTF-8编码的字节数组。
根据上面文章的讲解,咱们就能够讲清楚下面几个经常遇到的问题:
另外,java虚拟机对字符串的表示或处理不少都是使用的UTF-16编码或MUTF-8编码,而UTF-8编码通常是显式经过Java API层的String.getBytes("UTF-8")
函数获得。
平时使用时,若是只用Java语言开发通常不会有什么乱码问题,但若是本身想手动实现一个Java虚拟机,或是要经过JNI作一些事情的时候,就须要去了解一下Java的这些编码知识了。如笔者以前参与的项目,用go处理Java虚拟机的编码问题就很头大,由于Go这边默认使用的是UTF-8编码,因此若是在实现常量池的过程当中用Go的string去存储java.lang.String的实际内容,则可能出现一些奇怪的乱码问题。