UTF-16 -- 顶级程序员也会忽略的系统编码问题,JDK 错了十年!
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 编码。
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 错了十年!的更多相关文章
- 中国顶级程序员,从金山WPS走出来,自研了“表格编程”神器
程序员的圈子里有很多如明星般闪耀的牛人! 有中国"第一代程序员"--求伯君,有在微信获得巨大成功的张小龙,有图灵奖获得者姚期智,有商业巨子张一鸣,更有开源影响力人物--章亦春. 章 ...
- SQL Server 致程序员(容易忽略的错误)
标签:SQL SERVER/MSSQL/DBA/T-SQL好习惯/数据库/需要注意的地方/程序员/容易犯的错误/遇到的问题 概述 因为每天需要审核程序员发布的SQL语句,所以收集了一些程序员的一些常见 ...
- [转]程序员趣味读物:谈谈Unicode编码
from : http://pcedu.pconline.com.cn/empolder/gj/other/0505/616631_all.html#content_page_1 这是一篇程序员写给程 ...
- [百度空间] [转]程序员趣味读物:谈谈Unicode编码
出处:CSDN [ 2005-05-13 10:05:53 ] 作者:fmddlmyy 这是一篇程序员写给程序员的趣味读物.所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG ...
- 程序员趣味读物:谈谈Unicode编码
这是一篇程序员写给程序员的趣味读物.所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG游戏的升级.整理这篇文章的动机是两个问题: 问题一: 使用Windows记事本的“另存为 ...
- 国外一教授坦言,用这方法能迅速成为python程序员,但都不愿意说_编程小十
越来越多的人学习python,但你学习python用了多长的时间?#Python# 你知道如何才能迅速掌握并成为python程序员吗? 有这样的一位国外的教授说,要迅速成为python程序员,几乎 ...
- 如何像如何像 NASA 顶级程序员一样编程 — 10 条重要原则
https://www.oschina.net/news/90499/nasa-programmer-rule?from=20171112#0-qzone-1-7898-d020d2d2a4e8d1a ...
- 黑马程序员_JAVA之银行业务调度系统
------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 1.模拟实现银行业务调度系统逻辑,具体需求如下: 银行内有6个业务窗口,1 - 4号窗口为普通窗 ...
- Java程序员也应该知道的系统知识系列之(网卡,cpu,内存,硬盘,虚拟化)
https://yq.aliyun.com/articles/1718?spm=5176.100240.searchblog.16.UaGd04 https://yq.aliyun.com/artic ...
随机推荐
- 深入探索ScrollWindow
最近做WIN32 API开发时发现对ScrollWindow的一些工作原理并不是太清楚,于是做了相关研究,记载下来和大家共同学习. 首先在WM_CREATE中获取系统字符的宽度和高度 case WM_ ...
- es6基本语法,vue基本语法
一.es6基本语法 0.es6参考网站 http://es6.ruanyifeng.com/#README 1.let 和 const (1)const特点: 只在局部作用域起作用 不存在变量提升 不 ...
- Swagger API文档集中化注册管理
接口文档是前后端开发对接时很重要的一个组件.手动编写接口文档既费时,又存在文档不能随代码及时更新的问题,因此产生了像swagger这样的自动生成接口文档的框架.swagger文档一般是随项目代码生成与 ...
- ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理
在前面两篇随笔<ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理>和<ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程>开始 ...
- 一文看懂javaGC
javaGC回收机制 在面试java后端开发的时候一般都会问到java的自动回收机制(GC).在了解java的GC回收机制之前,我们得先了解下Java虚拟机的内存区域. java虚拟机运行时数据区 j ...
- Scala 学习之路(一)—— Scala简介及开发环境配置
一.Scala简介 1.1 概念 Scala全称为Scalable Language,即“可伸缩的语言”,之所以这样命名,是因为它的设计目标是希望伴随着用户的需求一起成长.Scala是一门综合了面向对 ...
- 2019.ccpc女生赛-wfinal总结
2019ccpc女生赛离它结束有四天了,在这个期间我想了很多,想了想还是决定写这个总结.作为这个队伍唯一的一名大一队员,我很庆幸,能跟着两个学姐一起打比赛,计爱玲师姐,即将工作,张莹俐学姐.这估计都是 ...
- Matplotlib快速入门
Matplotlib 可能还有小伙伴不知道Matplotlib是什么,下面是维基百科的介绍. Matplotlib 是Python编程语言的一个绘图库及其数值数学扩展 NumPy.它为利用通用的图形用 ...
- 大白话五种IO模型
目录 一.I/O模型介绍 二.阻塞I/O模型 2.1 一个简单的解决方案 2.2 该方案的问题 2.3 改进方案 2.4 改进后方案的问题 三.非阻塞I/O模型 3.1 非阻塞I/O实例 四.多路复用 ...
- 什么是JS跨域请求
这里说的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同域的框架中(iframe)的数据.只要协议.域名.端口有任何一个不同,都被 ...