一 疑问
- 什么是Emoji,跟Unicode什么关系,要搞懂emoji为什么要先理解Unicode?
- 什么是Unicode,跟ASCII什么关系?
- 大端序小端序是什么概念?哪些机器用大端序,哪些机器用小端序、
- 什么是编码?什么是码表?Java用的是什么编码?
二 编码字符集和字符编码表
-
编码字符集 (Coded Character Set 即 CCS)
编码字符集的概念就是,给现实世界中的字符,对应的映射一个数字。
这种映射,就是编码字符集。
例如 a=1, b=2, c=3。在计算机里面,将1、2、3分别代表a、b、c;当然这只是我自己一厢情愿的想法,并不会有人遵守这个约定。
现实世界中,最早的字符集是ASCII编码,a=61、b=62、c=63.
在ASCII后面出现的Unicode也属于这一范畴。 -
码点
在编码字符集中,字符对应的唯一的编码,称为码点。
例如ASCII中,a的码点就是61,b的码点是62. -
字符编码表 (Charater Encoding Form 即 CEF)
字符编码表的概念是,按照一定的规则,将编码字符集中的码点转换成一定长度的二进制序列。例如UTF-32、UTF-8、UTF-16就是属于这一概念的范畴。相同的字符内容,通过这些不同的编码,在计算机中所占的存储空间是不同的。 -
其实在Unicode出现之前,编码字符集和字符编码表是同一个概念,即字符对应的二进制数就是最终计算机中存储的数据。例如ASCII编码就是这样,a的码点是16进制的61,61转为二进制为1100001,在计算机中0110001就代表字符 “a”。
-
码元
码元可以理解为:不同的编码方式(CEF),对码点在计算机中存储和计算时的一个最小单位。一个码点可以映射为一个或者多个码元。
三 ASCII
-
ASCII即 American Standard Code for Information Interchange ,美国信息交换标准代码。就是最早的编码字符集。因为英文字母较少,所以美国科学家用7位比特(bit)来映射128个(2^7)字符。这128个字符中,有33个控制字符例如回车、删除等。其它的都是打印字符。
计算机读写最小单位是字节(Byte),一个字节是8比特(bit),ASCII码只用了7比特,于是ASCII码规定,缺少的1个比特,将最高位用0补齐。这样ASCII既是字符集,也是字符编码表。 -
字母“a”的ASCII码是61,这个61是16进制的,转换(在线转换工具https://tool.lu/hexconvert/)为10进制就是97。
在代码中,给char类型的aInt赋值97.会打印出a字符,
public class MainJava {
public static void main(String… args) {
System.out.println(“yq JAVA test”);
char aInt = 97;
System.out.println("aInt = " + aInt + ", hexString = " + Integer.toHexString(aInt));
}
}
输出结果:
yq JAVA test
aInt = a, hexString = 61
四. Unicode的出现,Unicode和emoji关系。
Unicode就是纯粹的字符集,跟编码方式无关。
-
最开始计算机只支持英语,用ASCII码确实够用了。但是随着发展,除了英语外,地球上其他非英语地区也都需要用计算机。于是世界各地都出现了不同的字符集和编码表,比如中国的GBK。
但是这样就出现了不统一的问题,一个地区的字符,按照它们自己的编码规则来映射和存储,到另外一个地区的编码规则来解析,就乱码了。 -
于是Unicode应运而生,Unicode就是对世界上所有的字符,都给它一个唯一编码。世界上所有的字符,都有一个唯一的编码,与什么语言、什么平台、什么程序无关。这样字符在传输和解析的时候,就不会乱码了。所以又叫统一码、单一码、万国码。
例如“天”的Unicode码点是22825(十进制,16进制是5929)。
public static void main(String… args) {
System.out.println(“yq JAVA test”);
char oneChar = ‘天’;
int oneInt = oneChar;
System.out.println("oneChar = " + oneChar + ", oneInt = " + oneInt + ", hexString = " + Integer.toHexString(oneChar));
}
输出:
yq JAVA test
oneChar = 天, oneInt = 22825, hexString = 5929
同时Unicode也给emoji分配了码点,就是一个数字代表一个emoji表情,比如十六进制数字1F600 代表笑脸😀这个emoji表情。
-
Unicode向前兼容ASCII码,承认ASCII占用0 – 127整数资源的合法性。并在其后接着占用 128 --65535的整数资源,来给世界上所有的字符分配唯一码点。
但是很快Unicode就发现65535远远不够,于是又把后面接着的16个65536整数资源给占用了。所以一共占用了17*65536个整数资源。 -
Unicode的表示形式是U+后面,跟4个或者6个十六进制数。
例如上面说的最开始的65535个整数,用Unicode的表现形式就是U+0000 – U+FFFF。 十六进制FFFF对应十进制的65535。
所以Unicode的整个范围,是17个65536的整数资源,也就是U+0000 – U+10FFFF。 -
平面、BMP、SP
Unicode发现65535不够用、又增加了16个65536的整数资源后,Unicode码点的全部范围就是17个65536部分。每一个65536的部分,称为一个平面(Plane)。第一个平面称为Plane0。
Basic Multiligual Pane 即基本多语言平面,就是第一个平面Plane0。范围是U+0000 – U+FFFF
FFFF转为二进制即:11111111 11111111,需要占用16比特,也就是2字节。
BMP内的U+D800到U+DFFF之间的码位区段,Unicode规定是永久保留的,不映射到具体的字符。这些码点陈为代理码点 Surrogate Code Point。
D800–DBFF属于高代理区(High Surrogate Area),后面的部分DC00–DFFF属于低代理区(Low Surrogate Area),各自的大小均为4×256=1024。
UTF-16就是用这些保留的码点,对辅助平面进行编码,具体的编码规则接下来再讲。
除了基本多语言平面,其它的叫做辅助平面(也叫增补平面) Supplementary Planes。辅助平面的范围是U+010000 – U+10FFFF,十进制的65536 到 1114111。
-
一些辅助平面中,还有很多空间没有分配字符,为了以后扩展。
-
CJK统一汉字
在BMP中,有一部分码点分配给汉字,称为CJK统一汉字(Chinese、Japanese and Korean中日韩)。这一部分区域的范围是 \u4E00-\u9FA5(19968-40869),这个范围包含了两万多个中文字符。但是这一范围并不能映射所有汉字,所以在增补平面中也有部分生僻汉字。
五 Unicode的编码方式
-
一个字符的Unicode码点是确定一致的,但是在为了节省空间的目的、以及不同的平台和系统的编码方式不同,对Unicode的编码方式并不同。
将Unicode码点数字值以某种规则,转换为另一种格式的数字,以便在计算机中储存和传输,就是对Unicode编码。 -
为什么要对Unicode编码呢?
电脑中存储和传输的内容都是二进制,Unicode也是以二进制的形式存在于计算机中。1 字节 = 8 比特。
怎么存放这些Unicode码点值,就是对Unicode编码。
Unicode编码称为Unicode Transformation Format,简称UTF。
”华“字的Unicode的编码值是U+534E,转为二进制是 1010011 01001110 一共7位比特,用2个字节表示就可以了(高为补0)。
Emoji表情笑脸“😀”的Unicode编码是U+1F600,转为二进制是 1 11110110 00000000 一共17位比特,并不能用2个字节,至少需要3个字节。
Unicode中码点值的范围是U+0000 – U+10FFFF,十六进制数 10FFFF 转为二进制是 10000 11111111 11111111 一共21位比特,至少需要3个字节。
Unicode中的有些字符需要2字节,有些字符又需要3字节,怎么在计算机中存储呢?
直接用4个字节(32位比特)来存放Unicode编码,不足的高位直接补0,这就是最直观简单方便的方式了。
UTF-32编码就是这样做的,无论Unicode码是多少,每个字符站4个字节。4字节刚好是32位,这也是UTF-32名称的由来。UTF-32是定长编码。
但是这样会有很多冗余,因为Unicode字符集中,我们常用的字符都在基本平面,范围U+0000 – U+FFFF只需要2个字节就可以了。
于是就有了UTF-16和UTF-8等编码方式。
六 UTF-16编码
- 最开始时UTF-16用2个字节来存储一个字符,2个字节刚好是16位,这也是UTF-16名称的由来。
对于基本平面中的字符,Unicode码点值小于等于FFFF,只需要2个字节就可以了。
Unicode扩充了增补平面后,对于超过FFFF的字符,2个字节就无法存储了,例如Emoji表情笑脸“😀”的Unicode编码是U+1F600。这时就会采用4个字节存储。
所以UTF-16是变长编码。
之前在平面一节有说到,Unicode规定了:基本平面中的 U+D800到U+DFFF是代理区,这些代理区并不存放具体的字符码点。
其中D800到DBFF属于高代理区,后面的部分DC00到DFFF属于低代理区。
如果将这些区域,按某种规则,是否可以存放补充平面中的Unicode码点呢?UTF-16就想到了一个这种规则,具体如下:
- 将高代理区和低代理区的排列组合起来,一起来表示一个辅助平面中的码点。其中高代理区D800到DBFF一共有1024种组合(DBFF减D800等于3FF转为十进制1023,再加1)。
低代理区也是一样,有1024种组合,两者合起来,一共是1024 X 1024 = 1048576种组合。
而辅助平面,刚好有1048576个字符码点(U+010000 到 U+10FFFF,也就是65536 到 1114111, 1114111 - 65536 + 1 = 1048576)。这样,代理区就能完全表达辅助平面中的所有字符了。
具体的换算规则(十六进制):
Lead = (码点 - 10000) ÷ 400 + D800
Trail = (码点 - 10000) % 400 + DC00
以Emoji表情笑脸“😀” (Unicode编码是U+1F600)为例:
Lead = (1F600 - 10000) ÷ 400 + D800 = D83D
Trail = (1F600 - 10000) % 400 + DC00 = DE00
所以其UTF-16编码为\uD83D\uDE00。在 https://www.bejson.com/convert/unicode_chinese/ 这个网站中,输入😀 并点击中文转Unicode,得到\ud83d\ude00,证明换算是正确的。
可以在代码中验证:
public static void main(String… args) {
int grinningInt = Integer.parseInt(“1F600”, 16);
char[] chars = Character.toChars(grinningInt);
for (int i = 0; i < chars.length; i++) {
char grinningChar = chars[i];
System.out.println("grinningChar = " + Integer.toHexString(grinningChar));
}
String grinningStr = new String(chars);
System.out.println("grinningInt = " + grinningInt + ", grinningStr = " + grinningStr);
}
输出:
grinningChar = d83d //高位
grinningChar = de00 //低位
grinningInt = 128512, grinningStr = 😀
-
读取时,怎么判断是读取2个字节,还是4个字节呢?
很简单,由于辅助平面中的字符码点值,转化后都是由高代理区和低代理区组成的一对值;每次读取2字节时,如果发现读取的值存在于高代码区则继续再读2个字节,一共读取4个字节。这样就能保证读取正确了。 -
许多编程语言内部的编码都是UTF-16,例如java、js、C#、python。
七 UTF-8编码
-
UTF-16编码虽然是变长编码,一个字符但是最小也要占用2字节。有相当多的字符,只需要一个字节就可以存储了,比如ASCII只占7位,只需要一个字节就可以存储了。
于是有了更精简的编码方式UTF-8。 -
UTF-8编码规则:
(1).对于单字节字符,字节的第一位设置为0,后面的7位为这个字符的Unicode码。因此对于ASCII码,UTF-8是完全一样的。
(2).对于n字节字符(n>1):第一个字节的前n位都设置为1,第1个字节的第n+1位设置为0。后面的所有字节,开头两位一律设置成10。剩下没有提及的二进制位,从低到高全部用字符的Unicode二进制位补齐。
UTF-8 编码方式 (二进制)
UTF-8 编码方式 (二进制) Unicode 符号范围
0xxxxxxx 0-127
110xxxxx 10xxxxxx 128-2047
1110xxxx 10xxxxxx 10xxxxxx 2048-65535
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-1114111
例如严的 Unicode 是 20005 (4E25)(100 111000 100101),根据上表,可以发现20005处在第三行的范围内,因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,严的 UTF-8 编码是11100100 10111000 10100101。
对单字符进行转换之后,字符串传输的时候直接拼接即可,切割的时候则先读取第一位的 1 的数量,来判断后面多少字节都是同一个字的,再进行切割。这样,如果中间有漏字符,也可以发现。
比如 “中国”的 UTF-8 表示为:
11100100 10111000 10101101 11100101 10011011 10111101
其实你可以发现,因为 UTF-8 加入了位数提示,所以会占用更多的长度来表达字符串。比如中文通常是 2048-65535 之间,所以一个中文在 UTF-8 会占用 3 个 8 位(3 字节)。而更加节约的 UTF-16 只用占用 2 个字节。但是 UTF-8 可以无误的表达 65535 之后的字符,这是 UTF-16 和 GBK 无法做到的。
在过去的标准里,UTF-8 最多可以用 6 个 8 位(6 字节)表示表示一个字符,然而 Unicode 也只能表示到 1114111,所以 UTF-8 也只需 4 位就足够了。
另外,因为用到 65536 之后的机会并不多。一些数据库 ,比如 Mysql,默认储存 UTF-8 时,就只给每个字符留了最多 3 位的空间。后面 Emoji 兴起后,Mysql 为了兼容之前的版本,不得不新增了一个数据类型 utf8mb4 来支持 4 位的 UTF8,这个功能在 Mysql 5.5.3 中加入。我们应该优先设定 Mysql 数据类型为 utf8mb4 。
八 字节序,大端序,小端序
-
字节序,即字节的顺序,是多字节数据在内存中存放的方式。单字节数据当然没有字节序的概念。
字节序分为大端序(Big Endian)和小端序(Little Endian)
小端序:数据的低位字节,存放在内存低位地址。
大端序:数据的低位字节,存放在内存高位地址。
例如有数据168496141,对应的十六进制数是0X0A0B0C0D,在内存地址从左到右依次增加。
大端序 0A0B0C0D
小端序 0D0C0B0A -
大端序符合人类的阅读习惯,所以网络通信中TCP/IP协议规定的是大端序。
而计算机读取时,一般都是从低位内存开始,先后分别读0D,0C,0B,0A效率高。所以计算机内部一般采用小端序。
ARM,Intel等就是用小端序。但也不是绝对,mac IOS就是采用大端序。
还有些机器,同时支持大端序小端序。 -
怎么判断机器是哪种端序?摘用网上的一段代码:
#include <stdio.h>
int main ()
{
unsigned int x = 0x12345678;
charc = (char)&x;
if(*c == 0x78) {
printf(“Little endian”);
} else{
printf(“Big endian”);
}
return 0;
}
九 几个工具网站:
unicode中文互转 https://www.bejson.com/convert/unicode_chinese/
ASCII编码转换 http://www.hiencode.com/cencode.html
进制转换 https://tool.lu/hexconvert/
参考文献
深入理解Emoji(一) —— 字符集,字符集编码 https://www.jianshu.com/p/8d675a5b9e5c
编码问题(上) (说的很详细 Unicode的来源、历史) https://www.cnblogs.com/codefuturedalao/p/14340385.html
字符集与编码(四)——Unicode https://my.oschina.net/goldenshaw/blog/310331
谈谈字符编码:Unicode、UTF-8 和 char[] (最通俗) https://luan.ma/post/character-encoding/
深入分析 Java 中的中文编码问题 https://www.cnblogs.com/bodhitree/p/9035142.html
彻底弄懂 Unicode 编码 https://blog.csdn.net/hezh1994/article/details/78899683
https://www.unicode.org/Public/emoji/14.0/emoji-sequences.txt
Full Emoji List, v14.0 https://unicode.org/emoji/charts/full-emoji-list.html
https://www.unicode.org/Public/emoji/14.0/emoji-test.txt
从Emoji的限制到Unicode编码 那些年的Emoji https://www.jianshu.com/p/64ec0f6b6245
深入理解Emoji(二) —— 字节序和BOM https://www.jianshu.com/p/ca191d9bdcc0
什么是大端序和小端序,为什么要有字节序? https://zhuanlan.zhihu.com/p/352145413
教你用golang判断大小端字节序 https://segmentfault.com/a/1190000039738719
脑残式网络编程入门(九):面试必考,史上最通俗大小端字节序详解 https://cloud.tencent.com/developer/article/1678869
Unicdoe【真正的完整码表】对照表(一) https://blog.csdn.net/hherima/article/details/9045765
【原创】经验分享:一个小小emoji尽然牵扯出来这么多东西?(最能看懂) https://juejin.cn/post/6881336349169811464