一、字符集(Charcater Set)与字符编码(Encoding)
- 字符集(Charcater Set 或 Charset):是一个系统支持的所有抽象字符的集合,也就是一系列字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。常见的字符集有: ASCII 字符集、Unicode 字符集等。
- 字符编码(Character Encoding):是一套法则,使用该法则能够对自然语言的字符的一个字符集(如字母表或音节表),与计算机能识别的二进制数字进行配对。即它能在符号集合与数字系统之间建立对应关系,是信息处理的一项基本技术。通常人们用符号集合(一般情况下就是文字)来表达信息,而计算机的信息处理系统则是以二进制的数字来存储和处理信息的。字符编码就是将符号转换为计算机能识别的二进制编码。
一般一个字符集等同于一个编码方式,ANSI 体系( ANSI 是一种字符代码,为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符)的字符集如 ASCII、ISO 8859-1、GB2312、 GBK 等等都是如此。一般我们说一种编码都是针对某一特定的字符集。一个字符集上也可以有多种编码方式,例如 UCS 字符集(也是 Unicode 使用的字符集)上有 UTF-8、UTF-16、UTF-32 等编码方式。
从计算机字符编码的发展历史角度来看,大概经历了三个阶段:
- 第一个阶段:ASCII 字符集和 ASCII 编码。 计算机刚开始只支持英语(即拉丁字符),其它语言不能够在计算机上存储和显示。ASCII 用一个字节( Byte )的 7 位(bit)表示一个字符,第一位置 0。后来为了表示更多的欧洲常用字符又对 ASCII 进行了扩展,又有了 EASCII,EASCII 用 8 位表示一个字符,使它能多表示 128 个字符,支持了部分西欧字符。
- 第二个阶段:ANSI 编码(本地化) 为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 ‘中’ 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。 不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。 不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
- 第三个阶段:UNICODE(国际化) 为了使国际间信息交流更加方便,国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。UNICODE 常见的有三种编码方式:UTF-8、UTF-16、UTF-32。
二、ASCII、Unicode字符集
1、ASCII:
我们知道,计算机内部,所有信息最终都是一个二进制值。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。
上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码(American Standard Code for Information Interchange),一直沿用至今。
ASCII 码一共规定了128个字符的编码,比如空格SPACE是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0。
2、非ASCII:
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
但是,这里又出现了新的问题。那就是不同的国家的字符集可能不同,就算它们都能用 256 个字符表示全,但是同一个码点(也就是 8 位二进制数)表示的字符可能可能不同。例如,144 在阿拉伯人的 ASCII 码中是 گ,而在俄罗斯的 ASCII 码中是 ђ。
因此,ASCII 码的问题在于尽管所有人都在 0 - 127 号字符上达成了一致,但对于 128 - 255 号字符上却有很多种不同的解释。
2.1)GB2312
亚洲语言有更多的字符需要被存储,一个字节已经不够用了。但是这难不倒智慧的中国人民,我们不客气地把那些 127 号之后的奇异符号们直接取消掉, 规定:
- 一个小于 127 的字符的意义与原来相同,但两个大于 127 的字符连在一起时,就表示一个汉字;
- 前面的一个字节(他称之为高字节)从 0xA1 用到 0xF7,后面一个字节(低字节)从 0xA1 到 0xFE;
这样我们就可以组合出大约 7000 多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的 全角字符。而原来在 127 号以下的那些就叫 半角字符 了。
中国人民看到这样很不错,于是就把这种汉字方案叫做 GB2312。GB2312 是对 ASCII 的中文扩展。
2.2)GBK
但是中国的汉字太多了,很快就发现有许多人名没有办法打出来。于是我们不得不继续把 GB2312 没有用到的码位找出来老实不客气地用上。
后来还是不够用,于是干脆不再要求低字节一定是 127 号之后的内码,只要第一个字节是大于 127 就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 GBK 标准,GBK 包括了 GB2312 的所有内容,同时又增加了近 20000 个新的汉字(包括繁体字)和符号。
2.3)GB18030 / DBCS
后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK 扩成了 GB18030。从此之后,中华民族的文化就可以在计算机时代中传承了。
中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做 DBCS(Double Byte Charecter Se)t:双字节字符集。
在 DBCS 系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于 127 的,那么就认为一个双字节字符集里的字符出现了。
3、Unicode:
因为当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码。最终,美国人意识到他们应该提出一种标准方案来展示世界上所有语言中的所有字符,出于这个目的,Unicode 诞生了。Unicode 源于一个很简单的想法:将全世界所有的字符包含在一个集合里,计算机只要支持这一个字符集,就能显示所有的字符,再也不会有乱码了。
Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字严。具体的符号对应表,可以查询unicode.org(Index),或者专门的汉字对应表(字体编辑用中日韩汉字Unicode编码表 - 编著:资深中韩翻译金圣镇 金圣镇)。
1)设计思路:
它从 0 开始,为每个符号指定一个编号,这叫做”码点”(code point)。比如,码点 0 的符号就是 null(表示所有二进制位都是 0)。
U+0000 = null
上式中,U+表示紧跟在后面的十六进制数是 Unicode 的码点。
2)基本平面和辅助平面:
这么多符号,Unicode 不是一次性定义的,而是分区定义。每个区可以存放 65536 个(2^16)字符,称为一个平面(plane)。目前,一共有 17 个平面,也就是说,整个 Unicode 字符集的大小现在是 2^21。
最前面的 65536 个字符位,称为基本平面(缩写 BMP),它的码点范围是从 0 一直到 2^16-1,写成 16 进制就是从 U+0000 到 U+FFFF。所有最常见的字符都放在这个平面,这是 Unicode 最先定义和公布的一个平面。
剩下的字符都放在辅助平面(缩写 SMP),码点范围从 U+010000 一直到 U+10FFFF。
Unicode 只规定了每个字符的码点,到底用什么样的字节序表示这个码点,就涉及到编码方法。
三、Unicode 编码方案——UTF-8、UTF-16
Unicode 没有规定字符对应的二进制码如何存储。以汉字“汉”为例,它的 Unicode 码点是 0x6c49,对应的二进制数是 110110001001001,二进制数有 15 位,这也就说明了它至少需要 2 个字节来表示。可以想象,在 Unicode 字典中往后的字符可能就需要 3 个字节或者 4 个字节,甚至更多字节来表示了。
这就导致了一些问题,计算机怎么知道你这个 2 个字节表示的是一个字符,而不是分别表示两个字符呢?这里我们可能会想到,那就取个最大的,假如 Unicode 中最大的字符用 4 字节就可以表示了,那么我们就将所有的字符都用 4 个字节来表示,不够的就往前面补 0。这样确实可以解决编码问题,但是却造成了空间的极大浪费,如果是一个英文文档,那文件大小就大出了 3 倍,这显然是无法接受的。
于是,为了较好的解决 Unicode 的编码问题, UTF-8 和 UTF-16 两种当前比较流行的变长编码方式诞生了。当然还有一个 UTF-32 的编码方式,也就是上述那种定长编码,字符统一使用 4 个字节,虽然看似方便,但是却不如另外两种编码方式使用广泛。
补充:历史上曾经出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。导致Unicode 在很长一段时间内无法推广,直到互联网的出现。
1、UTF-8:
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。
UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8 的编码规则很简单,只有二条:
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
- 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
下表总结了编码规则,字母x表示可用编码的位。
跟据上表,解读 UTF-8 编码非常简单。解码的过程也十分简单:如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面,还是以汉字严为例,演示如何实现 UTF-8 编码。
严的 Unicode 是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,严的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5。
2、UTF-16:
Windows 内核、Java、Objective-C (Foundation)、JavaScript 中都会将字符的基本单元定为两个字节的数据类型,也就是我们在 C / C++ 中遇到的 wchar_t 类型或 Java 中的 char 类型等等,这些类型占内存两个字节,因为 Unicode 中常用的字符都处于 0x0 - 0xFFFF 的范围之内,因此两个字节几乎可以覆盖大部分的常用字符。
UTF-16 编码介于 UTF-32 与 UTF-8 之间,同时结合了定长和变长两种编码方法的特点。它的编码规则很简单:基本平面的字符占用 2 个字节,辅助平面的字符占用 4 个字节。也就是说,UTF-16 的编码长度要么是 2 个字节(U+0000 到 U+FFFF),要么是 4 个字节(U+010000 到 U+10FFFF)。
1)那么问题来了,当我们遇到两个字节时,到底是把这两个字节当作一个字符还是与后面的两个字节一起当作一个字符呢?
这里有一个很巧妙的地方,在基本平面内,从 U+D800 到 U+DFFF 是一个空段,即这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。
辅助平面的字符位共有 2^20 个,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到 U+DBFF(空间大小 2^10),称为高位(H),后 10 位映射在 U+DC00 到 U+DFFF(空间大小 2^10),称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。
因此,当我们遇到两个字节,发现它的码点在 U+D800 到 U+DBFF 之间,就可以断定,紧跟在后面的两个字节的码点,应该在 U+DC00 到 U+DFFF 之间,这四个字节必须放在一起解读。
示例:
接下来,以汉字”𠮷”为例,说明 UTF-16 编码方式是如何工作的。
汉字”𠮷”的 Unicode 码点为 0x20BB7,该码点显然超出了基本平面的范围(0x0000 - 0xFFFF),因此需要使用四个字节表示。首先用 0x20BB7 - 0x10000 计算出超出的部分,然后将其用 20 个二进制位表示(不足前面补 0 ),结果为 0001000010 1110110111。接着,将前 10 位映射到 U+D800 到 U+DBFF 之间,后 10 位映射到 U+DC00 到 U+DFFF 即可。U+D800 对应的二进制数为 1101100000000000,直接填充后面的 10 个二进制位即可,得到 1101100001000010,转成 16 进制数则为 0xD842。同理可得,低位为 0xDFB7。因此得出汉字”𠮷”的 UTF-16 编码为 0xD842 0xDFB7。
2)Unicode3.0 中给出了辅助平面字符的转换公式:
H = Math.floor((c-0x10000) / 0x400)+0xD800
L = (c - 0x10000) % 0x400 + 0xDC00
根据编码公式,可以很方便的计算出字符的 UTF-16 编码。
以 𝌆 字符为例,它是一个辅助平面字符,码点为 U+1D306,将其转为 UTF-16 的计算过程如下。
H = Math.floor((0x1D306-0x10000)/0x400)+0xD800 = 0xD834
L = (0x1D306-0x10000) % 0x400+0xDC00 = 0xDF06
所以,字符的 UTF-16 编码就是 0xD834 0xDF06,长度为四个字节。
3、UTF-32:
UTF-32 是最直观的编码方法,每个码点使用四个字节表示,字节内容一一对应码点。比如,码点 0 就用四个字节的 0 表示,码点 597D 就在前面加两个字节的 0。
U+0000 = 0x0000 0000
U+597D = 0x0000 597D
UTF-32 的优点在于,转换规则简单直观,查找效率高。缺点在于浪费空间,同样内容的英语文本,它会比 ASCII 编码大四倍。这个缺点很致命,导致实际上没有人使用这种编码方法,HTML 5 标准就明文规定,网页不得编码成 UTF-32。
4、UCS-2 编码:
JavaScript 语言采用 Unicode 字符集,但是只支持一种编码方法。这种编码既不是 UTF-16,也不是 UTF-8,更不是 UTF-32。上面那些编码方法,JavaScript 都不用。JavaScript 用的是 UCS-2!
怎么突然杀出一个 UCS-2?这就需要讲一点历史。
互联网还没出现的年代,曾经有两个团队,不约而同想搞统一字符集。一个是 1988 年成立的 Unicode 团队,另一个是 1989 年成立的 UCS 团队。等到他们发现了对方的存在,很快就达成一致:世界上不需要两套统一字符集。
1991 年 10 月,两个团队决定合并字符集。也就是说,从今以后只发布一套字符集,就是 Unicode,并且修订此前发布的字符集,UCS 的码点将与 Unicode 完全一致。
UCS 的开发进度快于 Unicode,1990 年就公布了第一套编码方法 UCS-2,使用 2 个字节表示已经有码点的字符。(那个时候只有一个平面,就是基本平面,所以 2 个字节就够用了。)UTF-16 编码迟至 1996 年 7 月才公布,明确宣布是 UCS-2 的超集,即基本平面字符沿用 UCS-2 编码,辅助平面字符定义了 4 个字节的表示方法。
两者的关系简单说,就是 UTF-16 取代了 UCS-2,或者说 UCS-2 整合进了 UTF-16。所以,现在只有 UTF-16,没有 UCS-2。
那么,为什么 JavaScript 不选择更高级的 UTF-16,而用了已经被淘汰的 UCS-2 呢?
答案很简单:非不想也,是不能也。因为在 JavaScript 语言出现的时候,还没有 UTF-16 编码。
五、java中的码元和码点
Java 语言设计之初就认识到统一字符集( Unicode )的重要性,积极拥抱了问世不久的 Unicode 标准。Java 语言规定:Java 的 char 类型是 UTF-16 的 code unit ,也就是一定是16位(2字节);然后字符串是 UTF-16 code unit 的序列;这样,Java规定了字符要用UTF-16编码。
在 Java 编码体系中要区分清楚内码和外码:
- 内码是程序内部使用的字符编码,特别是某种语言实现其 char 或 String 类型在内存里用的内部编码;
- 外码是程序与外部交互时外部使用的字符编码。“外部”相对“内部”而言;不是 char 或 String 在内存里用的内部编码的地方都可以认为是“外部”。例如,外部可以是序列化之后的 char 或 String,或者外部的文件、命令行参数之类的。
1、java编码历史
Java最初设计的Charactor用两个字节来表示unicode字符,这没有问题, 因为最初unicode中的字符还比较少, Java 1.1之前采用Unicode version 1.1.5, JDK 1.1中支持Unicode 2.0, JDK 1.1.7支持Unicode 2.1, Java SE 1.4 支持 Unicode 3.0, Java SE 5.0开始支持Unicode 4.0。直到Unicode 3.0, Java用两个字节来表示unicode字符还没有问题,因为Unicode 3.0最多 49,259 个字符, 两个字节可以表示 65,535 个字符,还足够容的下所有的uicode3.0字符。但是Unicode 4.0(事实上自Unicode 3.1), 字符集进行很大的扩充,已经达到了96,447个字符,Unicode 11.0已经包含 137,374 个字符。随着Unicode字符集的不断完善,原本用来表示世界上所有字符的16个位在后期不能胜任这个任务(2个字节最多可以表示65535个字符)。所以,Java内部使用了一种可变长的,向后兼容的UTF-16。
1)码元和码点概念:
- Code Point: 代码点,一个字符的数字表示。一个字符集一般可以用一张或多张由多个行和多个列所构成的二维表来表示。二维表中行与列交叉的点称之为代码点,每个码点分配一个唯一的编号数字,称之为码点值或码点编号,除开某些特殊区域(比如代理区、专用区)的非字符代码点和保留代码点,每个代码点唯一对应于一个字符。从U+0000 到 U+10FFFF。
- Code Unit:代码单元,是指一个已编码的文本中具有最短的比特组合的单元。对于 UTF-8 来说,代码单元是 8 比特长;对于 UTF-16 来说,代码单元是 16 比特长。换一种说法就是 UTF-8 的是以一个字节为最小单位的,UTF-16 是以两个字节为最小单位的。
通俗来说:能表示一个字符的16位的(一个char)或n*16位(多个char)的编码称作码点,每个16位编码(每个char)称作码元。
2)java内部采用UTF-16编码:
前面介绍过UTF-16,采用2-4个字节存储:
- 典型的字符(可以在16位以内表示的字符):用一个16位编码来表示,占用一个char;
- 特殊的字符(占用位数超过16位的字符):用两个16位编码(称作surrogate characters)来表示,占用2个char;
为了实现 UTF-16 的变长编码语义,Java也引入码点和码元两个概念,Java 规定 char 仍然只能是一个 16 位的码元,也就是说 Java 的char类型不一定能表示一个UTF-16的“字符”, 只需1个码元的码点才可以完整的存在 char 里,而对于那些不常见的字符需要两个码元才能表示。
2、java中关于码元和码点的api:
String 作为 char 的序列,可以包含由两个码元组成的 “surrogate pair” 来表示需要 2 个码元表示的 UTF-16 码点,为此 Java 的标准库新加了一套用于访问码点的 API,而这套 API 就表现出了 UTF-16 的变长特性。
自 Java 1.5 java.lang.String就提供了Code Point方法, 用来获取完整的Unicode字符和Unicode字符数量:
- public int codePointAt(int index)
- public int codePointBefore(int index)
- public int codePointCount(int beginIndex, int endIndex)
- public int offsetByCodePoints(int index , int codePointOffset)
1)获取字符串中字符实际长度(码点个数):
string.length()返回的是Code Unit的长度,而不再是Unicode中字符的长度。对于传统的BMP平面的代码点,String.length和我们传统理解的字符的数量是一致的,对于扩展的字符,String.length可能是我们理解的字符长度的两倍。
要想获取字符串的字符实际长度(码点个数),可以使用public int codePointCount(int beginIndex, int endIndex) api:
private static void test1() {
// 中文常见字
String s = "你好";
System.out.println("1. string length =" + s.length()); //2
System.out.println("1. string char length =" + s.toCharArray().length); //2
System.out.println("1. codePointCount length =" + s.codePointCount(0, s.length())); //2
System.out.println();
// emojis
s = "👦👩";
System.out.println("2. string length =" + s.length()); //4
System.out.println("2. string char length =" + s.toCharArray().length); //4
System.out.println("2. codePointCount length =" + s.codePointCount(0, s.length())); //2
System.out.println();
// 中文生僻字
s = "𡃁妹";
System.out.println("3. string length =" + s.length()); //3
System.out.println("3. string char length =" + s.toCharArray().length); //3
System.out.println("3. codePointCount length =" + s.codePointCount(0, s.length())); //2
}
说明:对于普通的字符(一个码元表示的码点)组成的字符串,length()和codePointCount返回的值相等;如果字符串中有emoji或者生僻字(两个码元表示一个码点),只有codePointCount可以准确获取字符长度。
2)获取字符串中某个字符:
- string.charAt(n) :返回字符串中索引n的码元;
- string.codePointAt(n):返回字符串中索引n的码点(码点是一个int值);
private static void test2() {
String s = "你好";
System.out.println("charAt=" + s.charAt(0)); //你
System.out.println((int)s.charAt(0)); //20320
System.out.println("codePointAt=" + s.codePointAt(0)); //20320
s = "𡃁妹";
System.out.println(s.charAt(0)); //? 乱码
System.out.println(s.codePointAt(0)); //135361
String word2 = "𝕆";
String firstUnit = Integer.toHexString(word2.charAt(0));
String secondUnit = Integer.toHexString(word2.charAt(1));
String codePoint = Integer.toHexString(word2.codePointAt(0));
System.out.println("第一个单元:" + firstUnit + " 第二个:" + secondUnit + " 码点:" +codePoint);
}
3)字符串截取:
截取字符串时,有时候字符串会包含Emoji表情、以及一些特殊符号,用String的substring()进行截取操作,结果就有可能是乱码。这是因为JVM运行时使用UTF-16编码,对于普通的字符都是使用char类型存储(2个字节),而对于中文、emoji表情是用两个char存储(4个字节),substring是按照char截取的,就有可能只截取了半个中文字符,sting提供了offsetByCodePoints方法该方法返回此String 中从给定的 index 处偏移 codePointOffset 个Unicode代码点的索引,来辅助实现substring方法。
private static void test3() {
String s = "你好妹";
System.out.println(s.substring(1, 2)); //好
s = "你𡃁妹";
System.out.println(s.substring(1, 2)); //?
System.out.println(subStr(s, 1, 2)); //𡃁
}
private static String subStr(String value, int startIndex, int endIndex) {
String result;
if (StringUtils.isEmpty(value)) return "";
if (endIndex <= 0 || value.length() <= endIndex) return value;
try {
result = value.substring(value.offsetByCodePoints(0, startIndex),
value.offsetByCodePoints(0, endIndex));
} catch (Exception e) {
result = "";
}
return result;
}
3、emoji在mysql上的一个异常:
在 Android 手机或者 iPhone 的各种输入法键盘中,会自带一些 Emoji 表情符号,如 IPhone 手机系统键盘包含的表情符号有:
如果在移动端发布文本内容时包含了这种 Emoji 表情符号,通过接口传递到服务器端,服务器端再存入 MySQL 数据库:
- 对 gbk 字符集的数据库,写入数据库的数据,在回显时,变成 ‘口口’ 无法回显;
- 对 utf8 字符集的数据库,则根本无法写入数据库,程序直接报出异常信息 java.io.exception xxxxxxxx.
原因分析:
这是由于字符集不支持的异常,因为 Emoji 表情是四个字节,而 mysql 的 utf-8 编码最多三个字节,所以导致数据插不进去。真正的 utf8 编码(大家都使用的标准),最大支持 4 个 bytes。正是由于 mysql 的 utf8 少一个 byte,导致中文的一些特殊字符和 emoji 都无法正常的显示。mysql 真正的 utf8 其实是 utf8mb4,这是在 5.5.3 版本之后加入的。而目前的“utf8”其实是 utf8mb3。所以尽量不要使用默认的 utf8,使用 utf8mb4 才是正确的选择。
从 mysql 5.5.3 之后版本基本可以无缝升级到 utf8mb4 字符集。同时,utf8mb4 兼容 utf8 字符集,utf8 字符的编码、位置、存储在 utf8mb4 与 utf8 字符集里一样的,不会对有现有数据带来损坏。
字符编码:Unicode、UTF-8、GBK | Senit_Co