本文导读
很多人常常分不清UTF-8编码和UTF-16编码,或常常会问"Unicode编码和UTF-8编码有什么区别分割","Java的外码内码又是什么货色",这篇文章次要做一个对于编码常识的简略扫盲,包含对一些常见概念混同进行辨别解说。
根底概念
咱们常常提到的对于编码的概念能够粗略划分为两类:
- 字符集:将一个字符映射为某个惟一的数字(码值),如字符A在ascii码中映射为65
- 字符编码:将字符集用程序(字节)示意的一套规定,能够认为字符编码是字符集在计算机上的一种实现形式,如utf-8和utf-16都是unicode码的实现形式。
Unicode字符集介绍
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编码介绍
Utf-8全称为8-bit Unicode Transformation Format,是一种针对Unicode字符集的可变长编码,不同的Unicode码点会应用不同的字节数去存储,如ascii码(都小于128)则会应用1个字节去存储,一些罕用字符(如局部中文)会应用2~3个字节去存储,这有一些劣势,首先对于ascii码齐全兼容,且对于某些场景(只存在ascii码)编码后占用空间少,毛病也很显著,当遇到的都是须要占用3个字节存储的Unicode码点时,则会消耗更大的空间。
utf-8编码的根本规定如下:
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编码格局截取最初几位。
如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中。
0x0080-0xFFFF(0000 1000 0000 0000 — 1111 1111 1111 1111),截取后15位(无效示意位其实只有16位,因为从右往左第17位肯定是0),对应填到x中。
0x100000-0X10FFFF(0000 0001 0000 0000 0000 — 0001 0000 1111 1111 1111 1111),截取后21位(无效示意位其实只有21位,从右往左第22位肯定是0),对应填到x中。
因而,咱们能够失去所有Unicode的Utf-8编码规定。
UTF-16编码介绍
在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码,须要有一些非凡解决:
- 取后20位(减去10000),将这20位数字分为高10位和低10位,高、低10位的范畴即为0-0x3FF(00 0000 0000 — 11 1111 1111);
- 将高位加上0xD800,失去值范畴为0xD800—0xDBFF,将低位加上0xDC00,失去值范畴为0xDC00—0xDFFF;
- 将高位解决后的值(又称前导代理)放在前2个字节中,将低位解决后的值(后导代理)放在后2个字节中。
通过解决,前导代理和后导代理恰好占满了0xD800—0xDFFF这一段代理区域,这样解决的一个长处在于,看到每一个Utf-16编码,能够很分明地确定它是属于前导代理、后导代理还是除此以外的BMP区域中的Unicode。
MUTF-8编码介绍
MUTF-8(Modified UTF-8)编码,能够认为是对UTF-16编码的再编码。它的编码方式与UTF-8编码十分类似,只须要记住某些不同的状况,其余都与UTF-8编码统一。
具体的不同状况有二:
- 对于Unicode的0码点,UTF-8间接应用1个字节去存储(0000 0000),而MUTF-8会应用2个字节去存储,最初存储的值为0xC080(1100 0000 1000 0000)。
- 对于0x10000-0x10FFFF这块区域的Unicode码,之前提过UTF-8是应用4个字节去存储,而MUTF-8是对UTF-16的再编码,所以MUTF-8是对UTF-16编码的两个字符别离用3个字节去编码(因为这段区域的Unicode码值转为UTF-16编码后前导代理和后导代理的范畴是0xD800—0xDFFF,显著大于0x0800),共须要6个字节。
所以网上常常会提到UTF-8编码,又提到用1—6个字节去编码,其实说的是MUTF-8编码。
Java的内码与外码
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编码的字节数组。
总结
依据下面文章的解说,咱们就能够讲清楚上面几个经常遇到的问题:
- Unicode编码和UTF-8编码的区别? 其实Unicode只是字符集,而UTF-8是该字符集在计算机中的编码表示。
- 为什么说UTF-8是1~6个字节? 这里的UTF-8其实在指MUTF-8编码,MUTF-8应用1~3个字节对UTF-16编码进行再编码,所以就产生了应用6个字节示意一个Unicode字符的状况。
- Java的char到底占用几个字节?Java内码应用的是UTF-16编码,UTF-16对每个Unicode字符应用2或4个字节进行编码,所以对每个char单位,其实是占用了2个字节。
另外,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的理论内容,则可能呈现一些奇怪的乱码问题。