Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

  Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。目前的Unicode字符分为 17 组编排,0x0000 至 0x10FFFF,每组称为平面(Plane),而每平面拥有65536个码位,共1114112个。然而目前只用了少数平面。UTF-8、UTF-16、UTF-32 都是将数字转换到程序数据的编码方案。

  UTF-16 是 Unicode字符编码五层次模型的第三层:字符编码表的一种实现方式。即把 Unicode 字符集的抽象码位映射为 16 位长的整数的序列 (即码元),用于数据存储或传递。Unicode字符的码位,需要1个或者 2 个 16 位长的码元来表示,因此这是一个变长编码。

  上为 UTF-16 编码的介绍,红字部分强调它是一个变长编码;但实际上很多对编码有理解的人也都以为它是固定长度编码。这个错误的认知在日常编程中似乎不会有什么问题,因为常用中文字符都可以用 1 个 16 位长的码元表示。但是,中文有近 8 万个字符,而 1 个 16 位长的码元最大值仅是 65535(0xffff),所以超过一半的不常用中文字符被定义为扩展字符,这些字符需要用 2 个 16 位长的码元表示。

  UTF-16 编码以16位无符号整数为单位。我们把 Unicode 编码记作 U。编码规则如下:

  如果 U < 0x10000,U 的 UTF-16 编码就是 U 对应的16位无符号整数(为书写简便,下文将16位无符号整数记作WORD)。

  如果 U >= 0x10000,我们先计算 U' = U - 0x10000,然后将 U' 写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U 的 UTF-16 编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。

  为什么 U' 可以被写成 20 个二进制位?Unicode 的最大码位是 0x10FFFF,减去 0x10000 后,U' 的最大值是0xFFFFF,所以肯定可以用 20 个二进制位表示。例如:Unicode 编码 0x20C30,减去 0x10000 后,得到 0x10C30,写成二进制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的 y,用后 10 位依次替代模板中的 x,就得到:1101100001000011 1101110000110000,即 0xD843 0xDC30。

  按照上述规则,Unicode 编码 0x10000-0x10FFFF 的 UTF-16 编码有两个 WORD,第一个 WORD 的高 6 位是 110110,第二个 WORD 的高 6 位是 110111。可见,第一个 WORD 的取值范围(二进制)是 11011000 00000000到 11011011 11111111,即 0xD800-0xDBFF。第二个 WORD 的取值范围(二进制)是 11011100 00000000到 11011111 11111111,即 0xDC00-0xDFFF。

  为了将一个 WORD 的 UTF-16 编码与两 个WORD 的 UTF-16 编码区分开来,Unicode 编码的设计者将0xD800-0xDFFF 保留下来,并称为代理区(Surrogate):

D800-DBFF
High Surrogates
高位替代
DC00-DFFF
Low Surrogates
低位替代
 

  高位替代就是指这个范围的码位是两个WORD的UTF-16编码的第一个WORD。低位替代就是指这个范围的码位是两个WORD的UTF-16编码的第二个WORD。

  上述是对 UTF-16 编码规则的说明,那如何实现它呢?下面使用 C# 代码演示如何 UTF-16 与 UTF-32 间互转:

    public class Demo
{
internal const char HIGH_SURROGATE_START = '\ud800';
internal const char HIGH_SURROGATE_END = '\udbff';
internal const char LOW_SURROGATE_START = '\udc00';
internal const char LOW_SURROGATE_END = '\udfff'; internal const int UNICODE_PLANE00_END = 0x00ffff;
internal const int UNICODE_PLANE01_START = 0x10000;
internal const int UNICODE_PLANE16_END = 0x10ffff; public static bool IsHighSurrogate(char c)
{
return ((c >= HIGH_SURROGATE_START) && (c <= HIGH_SURROGATE_END));
} public static bool IsLowSurrogate(char c)
{
return ((c >= LOW_SURROGATE_START) && (c <= LOW_SURROGATE_END));
} public static char[] ConvertFromUtf32(int utf32)
{
// U+00D800 ~ U+00DFFF 这个范围被 Unicode 定义为专用代理区,它们不能作为 Unicode 编码值。
if ((utf32 < || utf32 > UNICODE_PLANE16_END) || (utf32 >= HIGH_SURROGATE_START && utf32 <= LOW_SURROGATE_END))
{
throw new ArgumentOutOfRangeException("utf32");
} if (utf32 < UNICODE_PLANE01_START)
{
// 这是一个基本字符。
return new char[] { (char)utf32 };
} // 这是一个扩展字符,需要将其转换为 UTF-16 中的代理项对。
utf32 -= UNICODE_PLANE01_START; return new char[]
{
(char)((utf32 / 0x400) + HIGH_SURROGATE_START),
(char)((utf32 % 0x400) + LOW_SURROGATE_START)
};
} public static int ConvertToUtf32(char highSurrogate, char lowSurrogate)
{
if (!IsHighSurrogate(highSurrogate))
{
throw new ArgumentOutOfRangeException("highSurrogate");
}
if (!IsLowSurrogate(lowSurrogate))
{
throw new ArgumentOutOfRangeException("lowSurrogate");
} return (((highSurrogate - HIGH_SURROGATE_START) * 0x400) + (lowSurrogate - LOW_SURROGATE_START) + UNICODE_PLANE01_START);
}
}

  为何说 JDK 在这一方面错了十年呢?因为在 Java 7 时期,因为字符串架构设计不合理,误 utf-16 将以为是固定长度编码,而实际 utf-16 是可变长度编码,因为 char(word) 的最大值是 0xffff,而 Unicode 规范最大值是 0x10ffff,小概率出现的字符需要两个 char 才能表示。Java 后来意识到这个错误,并 Java 接下来的几个版本里,匆匆将字符串编码改为 utf8(实际是,判断如果有字符超出 0xffff,则使用 utf-8,否则还是继续 不正常的 utf-16 算法)。再后来 Java 才使用上了正常的 utf-16 编码。

  前两年有个段子,说只有 2000 元以上的手机才能在输入法打出某个汉字。原因正是这个。
 
 
  这里附上由我的开源项目:.Net 平台的高性能 Json 解析库:Swifter.Json:https://github.com/Dogwei/Swifter.Json。希望大家支持一下。
 
  最后附上 Swifter.Json 内部使用的 Utf16 与 Utf8 互转的源代码:
    public static unsafe class EncodingHelper
{
public const char ASCIIMaxChar = (char)0x7f;
public const int Utf8MaxBytesCount = ; public static int GetUtf8Bytes(char* chars, int length, byte* bytes)
{
var destination = bytes; for (int i = ; i < length; i++)
{
int c = chars[i]; if (c <= 0x7f)
{
*destination = (byte)c; ++destination;
}
else if (c <= 0x7ff)
{
*destination = (byte)(0xc0 | (c >> )); ++destination;
*destination = (byte)(0x80 | (c & 0x3f)); ++destination;
}
else if (c >= 0xd800 && c <= 0xdbff)
{
c = ((c & 0x3ff) << ) + 0x10000; ++i; if (i < length)
{
c |= chars[i] & 0x3ff;
} *destination = (byte)(0xf0 | (c >> )); ++destination;
*destination = (byte)(0x80 | ((c >> ) & 0x3f)); ++destination;
*destination = (byte)(0x80 | ((c >> ) & 0x3f)); ++destination;
*destination = (byte)(0x80 | (c & 0x3f)); ++destination;
}
else
{
*destination = (byte)(0xe0 | (c >> )); ++destination;
*destination = (byte)(0x80 | ((c >> ) & 0x3f)); ++destination;
*destination = (byte)(0x80 | (c & 0x3f)); ++destination;
}
} return (int)(destination - bytes);
} [MethodImpl(VersionDifferences.AggressiveInlining)]
public static int GetUtf8Chars(byte* bytes, int length, char* chars)
{
var destination = chars; var current = bytes;
var end = bytes + length; for (; current < end; ++current)
{
if ((*((byte*)destination) = *current) > 0x7f)
{
return GetGetUtf8Chars(current, end, destination, chars);
} ++destination;
} return (int)(destination - chars);
} [MethodImpl(MethodImplOptions.NoInlining)]
private static int GetGetUtf8Chars(byte* current, byte* end, char* destination, char* chars)
{
if (current + Utf8MaxBytesCount < end)
{
end -= Utf8MaxBytesCount; // Unchecked index.
for (; current < end; ++current)
{
var byt = *current; if (byt <= 0x7f)
{
*destination = (char)byt;
}
else if (byt <= 0xdf)
{
*destination = (char)(((byt & 0x1f) << ) | (current[] & 0x3f)); ++current;
}
else if (byt <= 0xef)
{
*destination = (char)(((byt & 0xf) << ) | ((current[] & 0x3f) << ) + (current[] & 0x3f)); current += ;
}
else
{
var utf32 = (((byt & 0x7) << ) | ((current[] & 0x3f) << ) | ((current[] & 0x3f) << ) + (current[] & 0x3f)) - 0x10000; *destination = (char)(0xd800 | (utf32 >> )); ++destination;
*destination = (char)(0xdc00 | (utf32 & 0x3ff)); current += ;
} ++destination;
} end += Utf8MaxBytesCount;
} // Checked index.
for (; current < end; ++current)
{
var byt = *current; if (byt <= 0x7f)
{
*destination = (char)byt;
}
else if (byt <= 0xdf && current + < end)
{
*destination = (char)(((byt & 0x1f) << ) | (current[] & 0x3f)); ++current;
}
else if (byt <= 0xef && current + < end)
{
*destination = (char)(((byt & 0xf) << ) | ((current[] & 0x3f) << ) + (current[] & 0x3f)); current += ;
}
else if (current + < end)
{
var utf32 = (((byt & 0x7) << ) | ((current[] & 0x3f) << ) | ((current[] & 0x3f) << ) + (current[] & 0x3f)) - 0x10000; *destination = (char)(0xd800 | (utf32 >> )); ++destination;
*destination = (char)(0xdc00 | (utf32 & 0x3ff)); current += ;
} ++destination;
} return (int)(destination - chars);
} public static int GetUtf8CharsLength(byte* bytes, int length)
{
int count = ; for (int i = ; i < length; i += bytes[i] <= 0x7f ? : bytes[i] <= 0xdf ? : )
{
++count;
} return count;
} public static int GetUtf8MaxBytesLength(int charsLength)
{
return charsLength * Utf8MaxBytesCount;
} [MethodImpl(VersionDifferences.AggressiveInlining)]
public static int GetUtf8MaxCharsLength(int bytesLength)
{
return bytesLength;
}
}

UTF-16 -- 顶级程序员也会忽略的系统编码问题,JDK 错了十年!的更多相关文章

  1. 中国顶级程序员,从金山WPS走出来,自研了“表格编程”神器

    程序员的圈子里有很多如明星般闪耀的牛人! 有中国"第一代程序员"--求伯君,有在微信获得巨大成功的张小龙,有图灵奖获得者姚期智,有商业巨子张一鸣,更有开源影响力人物--章亦春. 章 ...

  2. SQL Server 致程序员(容易忽略的错误)

    标签:SQL SERVER/MSSQL/DBA/T-SQL好习惯/数据库/需要注意的地方/程序员/容易犯的错误/遇到的问题 概述 因为每天需要审核程序员发布的SQL语句,所以收集了一些程序员的一些常见 ...

  3. [转]程序员趣味读物:谈谈Unicode编码

    from : http://pcedu.pconline.com.cn/empolder/gj/other/0505/616631_all.html#content_page_1 这是一篇程序员写给程 ...

  4. [百度空间] [转]程序员趣味读物:谈谈Unicode编码

    出处:CSDN [ 2005-05-13 10:05:53 ] 作者:fmddlmyy 这是一篇程序员写给程序员的趣味读物.所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG ...

  5. 程序员趣味读物:谈谈Unicode编码

    这是一篇程序员写给程序员的趣味读物.所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG游戏的升级.整理这篇文章的动机是两个问题: 问题一: 使用Windows记事本的“另存为 ...

  6. 国外一教授坦言,用这方法能迅速成为python程序员,但都不愿意说_编程小十

    越来越多的人学习python,但你学习python用了多长的时间?#Python# 你知道如何才能迅速掌握并成为python程序员吗?   有这样的一位国外的教授说,要迅速成为python程序员,几乎 ...

  7. 如何像如何像 NASA 顶级程序员一样编程 — 10 条重要原则

    https://www.oschina.net/news/90499/nasa-programmer-rule?from=20171112#0-qzone-1-7898-d020d2d2a4e8d1a ...

  8. 黑马程序员_JAVA之银行业务调度系统

    ------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 1.模拟实现银行业务调度系统逻辑,具体需求如下: 银行内有6个业务窗口,1 - 4号窗口为普通窗 ...

  9. Java程序员也应该知道的系统知识系列之(网卡,cpu,内存,硬盘,虚拟化)

    https://yq.aliyun.com/articles/1718?spm=5176.100240.searchblog.16.UaGd04 https://yq.aliyun.com/artic ...

随机推荐

  1. 快速开发平台 WebBuilder 8 发布

    快速开发平台  WebBuilder 8 发布 了解:http://www.putdb.com,官网:http://www.geejing.com 什么是WebBuilder? WebBuilder是 ...

  2. Nginx反向代理负载均衡的容器化部署

    首先,在home目录创建microservices目录,开启第一篇章. cd ~ && mkdir microservices && cd microservices ...

  3. 极简代理IP爬取代码——Python爬取免费代理IP

    这两日又捡起了许久不碰的爬虫知识,原因是亲友在朋友圈拉人投票,点进去一看发现不用登陆或注册,觉得并不复杂,就一时技痒搞一搞,看看自己的知识都忘到啥样了. 分析一看,其实就是个post请求,需要的信息都 ...

  4. Spring Boot:使用Redis存储技术

    综合概述 Redis是一个开源免费的高性能key-value数据库,读取速度达110000次/s,写入速度达81000次/s.Redis支持丰富的数据类型,如Lists, Hashes, Sets 及 ...

  5. 最全java多线程总结3——了解阻塞队列和线程安全集合不

      看了前两篇你肯定已经理解了 java 并发编程的低层构建.然而,在实际编程中,应该经可能的远离低层结构,毕竟太底层的东西用起来是比较容易出错的,特别是并发编程,既难以调试,也难以发现问题,我们还是 ...

  6. NumPy基础操作

    NumPy基础操作(1) (注:记得在文件开头导入import numpy as np) 目录: 数组的创建 强制类型转换与切片 布尔型索引 结语 数组的创建 相关函数 np.array(), np. ...

  7. Mac上使用ssh连接服务器title显示服务器的ip

    Mac上使用ssh连接服务器title显示服务器的ip 使用Mac开发时,管理的服务器过多时,会搞混乱.可能有时啪啪啪一顿操作,最后发现操作错了机器. 解决方案 在远程服务器上,编辑vim /etc/ ...

  8. resolv.conf 的超时(timeout)与重试(attempts)机制

    /etc/resolv.conf 有两个默认的值至关重要,一个是超时的 timeout,一个是重试的 attempts,默认情况下,前者是 5s 后者是 2 次.这个估计很多工程师都不是很在意,一般情 ...

  9. Java基础中字符串与字符的注意点!

    在Java中,字符的表达与字符串的表达是不一样的!话不多说,直接上代码!!! String  a="a"; char a='a'; 其中他们的引号是不一样的

  10. SQL Server温故系列(3):SQL 子查询 & 公用表表达式 CTE

    1.子查询 Subqueries 1.1.单行子查询 1.2.多行子查询 1.3.相关子查询 1.4.嵌套子查询 1.5.子查询小结及性能问题 2.公用表表达式 CTE 2.1.普通公用表表达式 2. ...