本文主要介绍了UTF8的一些基本概念,简要介绍了mysql中 utf8 utf8mb3 utf8mb4 的区别;而后为介绍Java对Unicode编码的支持,引入了一些编码的基本概念,包括code point, code unit等,并介绍了Java提供的经常使用的支持Unicode编码的方法;最后给出了过滤UTF8mb4的方案html
UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它能够用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须作少部分修改,便可继续使用。所以,它逐渐成为电子邮件、网页及其余存储或发送文字的应用中,优先采用的编码。java
UTF-8使用一至四个字节为每一个字符编码(2003年11月UTF-8被RFC 3629从新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节):mysql
128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。sql
带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则须要两个字节编码(Unicode范围由U+0080至U+07FF)。数据库
其余基本多文种平面(BMP, Basic Multilingual Plane)中的字符(这包含了大部分经常使用字,例如CJVK经常使用字字符集 —— Chinese, Japanese, Vietnam, Korean)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。api
其余使用极少的Unicode 辅助平面(Supplementary Multilingual Plane)的字符使用四字节编码(Unicode范围由U+10000至U+10FFFF,主要包括不经常使用的CJK字符, 数学符号, emoji表情等)。oracle
utf-8编码方式
app
unicode code point table
ui
参考与扩展:
维基百科 UTF-8 https://en.wikipedia.org/wiki/UTF-8, 中文版 https://zh.wikipedia.org/wiki/UTF-8
维基百科 Plane_(Unicode) https://en.wikipedia.org/wiki/Plane_%28Unicode%29
维基百科 CJK characters https://en.wikipedia.org/wiki/CJK_characters
维基百科 Emoji https://en.wikipedia.org/wiki/Emoji编码
utf8编码是unicode编码的一种实现,能够简单的理解为unicode编码定义一串数字来一一对应咱们用到的字符,utf8定义了如何将unicode定义的这串数字保存到内存中。 另外须要强调的是utf8是一种变长的编码规范。
unicode 的范围 U+0000 - U+10FFFF。
参考与扩展
维基百科 Unicode https://en.wikipedia.org/wiki/Unicode
utf8mb4, MySQL在5.5.3以后增长了这个utf8mb4的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode字符。
mysql中的utf8,就是最大3字节的unicode字符,也就是mysql中的utf8mb3.
参考
mysql-charset-unicode-utf8mb3 https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb3.html and https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8.html
mysql-charset-unicode-utf8mb4 https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
表示范围:
说明 | mysql utf8 / utf8mb3 | mysql utf8mb4 |
---|---|---|
max bit | 3 | 4 |
范围 | 基本多文种平面 + US-ASCII | 辅助平面(Supplementary) + 基本多文种平面 + US-ASCII |
unicode范围 | U+0000 - U+FFFF | U+0000 - U+10FFFFF |
常见字符 | 英文字母,CJK大部分经常使用字等 | CJK很是用字,数学符号,emoji表情等 |
那么问题来了,若是用了utf8mb3编码的mysql数据库,在插入一些4字节长的字符时就会报错(形如:"java.sql.SQLException: Incorrect string value: '\xF0\x9F\x94\x91\xE6\x9D...' for column 'core_data' at row 1" 的错误),后文会介绍如何在Java中过滤掉这些字符。
要在Java中过滤Mysql的utf8mb4,必须弄清Java是如何支持Unicode编码,接下来徐徐展开......
下面先介绍几个概念:character(字符), character set(字符集), coded character set(字符编码集), code point(代码点), code space(代码空间),character encoding scheme(字符编码方案),code unit(编码单元),和3种Unicode经常使用的编码方式。
Unicode经常使用的三种编码方式 UTF-8, UTF-16, UTF-32, 下面以辅助平面中的字符'🔑' 为例作一个简要的介绍, 它的code point为128273(0x1F511):
utf8,编码单元为8bit,使用1-4个编码单元来表示Unicode中的字符,辅助平面中的字符在utf8中须要用4字节表示,对照前面的utf-8编码方案中4字节的编码格式, 从高到低依次为:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx, 因此其编码是编码是 '11110000 10011111 10010100 10010001',注意并非 0x1F511的二进制表示,不要混淆
utf16, 编码单元是16bit,用1-2个编码单元来表示Unicode中的字符,U+0000-U+FFFF(BMP)用一个编码单元表示,0x10000-0x10FFFF(SMP)用两个编码单元(high-surrogates和low-surrogates)表示,high-surrogates范围U+D800-U+DBFF,low-surrogates范围U+DC00-U+DFFF,编码方式见下文图片,编码结果为'11011000 00111101 11011101 00010001'。在Unicode编码中U+D800-U+DFFF是专门为UTF16保留的区间,没有分配其它字符,因此不用担忧一个code point有两个含义的问题。
utf32,编码半圆是32bit,能够只用一个编码单元来表示所有的Unicode字符,其编码就是 code point的值,也就是 '00000000 00000001 11110101 00010001'。
UTF-8编码方式
UTF-16编码方式
打印编码的code:
@Test public void printCharacterCode() { String s = "\uD83D\uDD11"; //字符'🔑' log.info("UTF8: {}", bytesToBits(s.getBytes(Charset.forName("utf-8")))); log.info("UTF16: {}", bytesToBits(s.getBytes(Charset.forName("utf-16")))); log.info("UTF32: {}", bytesToBits(s.getBytes(Charset.forName("utf-32")))); } public static String byteToBit(byte b) { return "" + (byte) ((b >> 7) & 0x1) + (byte) ((b >> 6) & 0x1) + (byte) ((b >> 5) & 0x1) + (byte) ((b >> 4) & 0x1) + (byte) ((b >> 3) & 0x1) + (byte) ((b >> 2) & 0x1) + (byte) ((b >> 1) & 0x1) + (byte) ((b >> 0) & 0x1); } public static String bytesToBits(byte[] bytes) { String s = ""; for (byte b : bytes) { s += byteToBit(b) + " "; } return s; }
使用上面的代码打印结果以下:
UTF8: 11110000 10011111 10010100 10010001 UTF16: 11111110 11111111 11011000 00111101 11011101 00010001 UTF32: 00000000 00000001 11110101 00010001
能够看到utf-16的结果并不是咱们期待的'11011000 00111101 11011101 00010001', 前面多了一个编码单元 'FEFF', 这个是这个是Unicode编码中的 BOM(byte order mark)位,用来表示byte(注意不是bit)的顺序,BOM是可选的,若是用那么它必须出如今字符串的开始(在其它编码中BOM不会出如今字符串开始,因此能够用来识别字符串是否Unicode编码)。
为何要用BOM位?为了标识编码单元的字节序,例如:“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59,若是咱们收到UTF-16字节流“594E”,那么这是“奎”仍是“乙”? 若是字符串的字节码是 'FEFF 4E59',那么则表示大端在左(big-endian),这个字是“乙”。
Unicode定义的6种BOM位
BOM位是能够缺省的,缺省时默认大端在左。
UTFs的属性概括
参考与扩展
Supplementary Characters in the Java Platform http://www.oracle.com/us/technologies/java/supplementary-142654.html
Unicode surrogate programming with the Java language https://www.ibm.com/developerworks/library/j-unicode/
微机百科 UTF16 https://zh.wikipedia.org/wiki/UTF-16
维基百科 code-point https://en.wikipedia.org/wiki/Code_point
D000-DFFF编码表 http://jicheng.tw/hanzi/unicode.html?s=D000&e=DFFF
utf bom http://unicode.org/faq/utf_bom.html
最初Unicode的编码数量并无超过65,535 (0xFFFF),早期Java版本中使用16bit的char表示当时所有的Unicode字符。后来Unicode字符集扩展到了1,114,111 (0x10FFFF)(在Unicode标准2.0用引入了辅助编码平面SMP,在3.1首次为SMP的部分编码分配了字符), JAVA中的char已经不足以表示Unicode的所有编码(须要32bit),JSR-204的专家讨论了不少方法想要解决这个问题,其中包括:
前文提到了UTF16用两个编码单元来表示超过U+FFFF的1,048,576 (1024*1024)个字符,Java中与之对应的概念就是"代理对(surrogate pair)"。
下面介绍Java中几个经常使用的code point(int)和char的转换方法
下面是一个简单的例子:
@Test public void testConverterOfCodePointAndChar() { String s = "a中\uD83D\uDD11a中"; for (int i = 0; i < s.codePointCount(0, s.length()); i++) { int codePoint = s.codePointAt(i); log.info("code point at {}: {},\t isSupplementaryCodePoint:{}", i, codePoint, Character.isSupplementaryCodePoint(codePoint)); } for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); log.info("char at {}: {},\t isSurrogate:{},\t isHighSurrogate:{},\t isLowSurrogate:{}, ", i, c, Character.isSurrogate(c), Character.isHighSurrogate(c), Character.isLowSurrogate(c)); } }
输出结果为:
code point at 0: 97, isSupplementaryCodePoint:false code point at 1: 20013, isSupplementaryCodePoint:false code point at 2: 128273, isSupplementaryCodePoint:true code point at 3: 56593, isSupplementaryCodePoint:false code point at 4: 97, isSupplementaryCodePoint:false char at 0: a, isSurrogate:false, isHighSurrogate:false, isLowSurrogate:false char at 1: 中, isSurrogate:false, isHighSurrogate:false, isLowSurrogate:false char at 2: ?, isSurrogate:true, isHighSurrogate:true, isLowSurrogate:false char at 3: ?, isSurrogate:true, isHighSurrogate:false, isLowSurrogate:true char at 4: a, isSurrogate:false, isHighSurrogate:false, isLowSurrogate:false char at 5: 中, isSurrogate:false, isHighSurrogate:false, isLowSurrogate:false
上面的例子中咱们看到一个奇怪的现象,codePointCount获取的字符的个数是对的,可是经过codePointAt去获取时,遇到SMP字符不会自动计算为两个代码单元,从源码(见附录)中能够看到
@Test public void testIterateCodePoint() { String s = "a中\uD83D\uDD11a中"; for (int i = 0; i < s.length(); i++) { int codePoint = s.codePointAt(i); log.info("code point at {}: {},\t isSupplementaryCodePoint:{}", i, codePoint, Character.isSupplementaryCodePoint(codePoint)); if (Character.isSupplementaryCodePoint(codePoint)) i++; } }
输出结果为:
code point at 0: 97, isSupplementaryCodePoint:false code point at 1: 20013, isSupplementaryCodePoint:false code point at 2: 128273, isSupplementaryCodePoint:true code point at 4: 97, isSupplementaryCodePoint:false code point at 5: 20013, isSupplementaryCodePoint:false
在理解了前面的概念后,我想再过滤掉4字长的UTF-8字符已经不难了吧。
4字长的UTF-8字符就是Unicode SMP(辅助平面)中的字符, 也就是Unicode编码大于U+FFFF的字符, 因此咱们只须要获取字符串中各个字符的code point,当code point 大于FFFF时(或者直接使用Character.isSupplementaryCodePoint来判断),过滤掉便可,示例代码以下:
@Test public void filterUtf8mb4Test() { String s = "a中\uD83D\uDD11a中"; log.info(filterUtf8mb4(s)); } public static String filterUtf8mb4(String str) { final int LAST_BMP = 0xFFFF; StringBuilder sb = new StringBuilder(str.length()); for (int i = 0; i < str.length(); i++) { int codePoint = str.codePointAt(i); if (codePoint < LAST_BMP) { sb.appendCodePoint(codePoint); } else { i++; } } return sb.toString(); }
输出结果为:
a中a中
String的 codePointCount 和 codePointAt 源码:
public int codePointCount(int beginIndex, int endIndex) { if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) { throw new IndexOutOfBoundsException(); } return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex); } public int codePointAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return Character.codePointAtImpl(value, index, value.length); }
它们调用的Character的 codePointCountImpl 和 codePointAtImpl 的源码:
static int codePointCountImpl(char[] a, int offset, int count) { int endIndex = offset + count; int n = count; for (int i = offset; i < endIndex; ) { if (isHighSurrogate(a[i++]) && i < endIndex && isLowSurrogate(a[i])) { n--; i++; } } return n; } static int codePointAtImpl(char[] a, int index, int limit) { char c1 = a[index]; if (isHighSurrogate(c1) && ++index < limit) { char c2 = a[index]; if (isLowSurrogate(c2)) { return toCodePoint(c1, c2); } } return c1; }