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. C++大小写转换和性能(C语言,C++,API,STL一共4种方法)

    大小写转换和性能 前言 本文主要讨论最基本的一些大小写转换函数和API,不讨论一些常见的字符串程序库里面的大小写转换接口,另外本文的落脚点是这些转换函数的性能和日常开发中遇到的一些问题. 不考虑范围 ...

  2. Memcached在Linux系统下的安装和PHP开启 Memcached的 扩展 超级解决方案

    [项目背景]:阿里云ECS服务器,Linux(centos7.2 64位),环境部署使用的是阿里云一键安装包(LAMP)等 [项目需求]:linux安装memcached 和php开启Memcache ...

  3. 快速开发平台 WebBuilder 8.4 发布

    WebBuilder是一款强大,全面和高效的应用开发和运行平台.基于浏览器的集成开发环境,可视化和智能化的设计,能轻松完成常规应用和面向手机的移动应用开发.高效.稳定和可扩展的特点,适合复杂企业级应用 ...

  4. java-mysql(3) 读写image

    在mysql里面用来存储图片有一个特殊的数据对象叫做 Blob(Binary Large Object). 数据库里面插入一张图片: 第一步:需要为图片创建一个文件对象 File img = new ...

  5. shell日期整理

    date 当前日期+时间 # 日期格式化:date+"" - date +"%Y%m%d" 不带横杠分隔符的日期20160107 date +"%Y% ...

  6. Vim入门操作整理

    根据小甲鱼的vim入门视频整理,供查阅 移动指令:上下左右 k j h l 翻页: ctrl + b  ctrl + f 保存退出:ZZ 普通模式:vim fileName 首次进入的就是普通模式 从 ...

  7. 29 z-index

    这个东西非常简单,它有四大特性,每个特性你记住了,页面布局就不会出现找不到盒子的情况. z-index 值表示谁压着谁,数值大的压盖住数值小的, 只有定位了的元素,才能有z-index,也就是说,不管 ...

  8. python trojan development 2nd —— use python to send mail and listen to the key board then combine them

    请勿用于非法用途!!!!!本人概不负责!!!原创作品,转载说明出处!!!!! from pynput.keyboard import Key,Listener import logging impor ...

  9. MyBatis 源码分析

    MyBatis 运行过程 传统的 JDBC 编程查询数据库的代码和过程总结. 加载驱动. 创建连接,Connection 对象. 根据 Connection 创建 Statement 或者 Prepa ...

  10. java多线程死锁

    进程(线程)同步的基本概念 进程之间的制约关系 1. 直接制约关系(进程同步) 这个关系主要源于进程合作,例如,有一个输入进程A通过单缓冲向进程B提供数据,当该缓冲空时,进程B因为不能获得所需数据而被 ...