详解java中的unicode编码(码点)

致谢:

本文参考网页:
Unicode字符集以及UTF-8,UTF-16编码的总结 - vcj1009784814的博客 - CSDN博客

Unicode

unicode的码点从U+0000到U+10FFFF,即共有2^20+2^16-1=1114111个码点。
通常来说,常见字符可以用2个字节(16位)来表示。但还有很多其他表意字符、辅助字符需要表示。

Unicode将所有码点分成了17个代码级别,又称平面

unicode的17个平面

其中第一个平面称为基本的多语言级别(basic multilingual plane, BMP),其他的统称为辅助平面

unicode的平面划分

utf-8

由于unicode会有1个字节-4个字节(最多32位)不等,如果定长存储每个字符都需要占用四个字节,十分浪费空间,utf-8是一种变长存储方式,有1-6个字节不等。具体编码方式如下:

  • 第一个字节提示了这个unicode编码由几个字节组成
    1. 首字节以0开头,表示单字节编码
    2. 首字节以110开头,表示双字节编码,后续字节以10开头
    3. 首字节以1110开头,表示三字节编码,后续字节以10开头
    4. 首字节以11110开头,表示四字节编码,后续字节以10开头
    5. ...
  • 有效位对应的字节数

有效位对应的字节数

utf-16

UTF-16源于UCS-2,UCS-2将字符码点直接映射为字符编码,中间无特别的编码算法。

UCS-2编码方式固定2字节编码,只覆盖了BMP的码点,对于SMP的码点,2字节的16位二进制数是不足以表示的。

而UTF-16扩展了原来的UCS-2编码,解决了辅助平面码点的字符无法表示的问题:

  • BMP中的有效码点,用固定2字节16位来为其编码,数值等于对应的码点,同UCS-2
  • 辅助平面中的有效码点,使用代理对进行编码。在BMP中,有一个范围的码点是未定义的,被称为代理区,其码点范围是0xD800~0xDFFF,共211个码点,代理区又被分为高代理码点低代理码点,其中高代理码点范围是0xD800~0XDBFF,低代理码点范围是0xDC00~0XDFFF,高代理码点和低代理码点结合在一起,就表示一个辅助平面中的字符。由于辅助平面中的字符共有220个(0x10000~0x10FFFF),高代理码点和低代理码点皆有210个取值,两者结合,恰好有220种不同的组合。

详解java中的unicode编码(码点)

也就是说,UTF-16可以表示完unicode中的字符,BMP中的字符需要两个字节,其他的需要四个字节。

java中的unicode

这里使用到一个工具:
Unicode编码转换,UTF编码转换(UTF-8、UTF-16、UTF-32)

char

java中的char是定长以16位(两个字节存储的),并且内部编码为utf-16。
也就是说,一个char只能表示BMP中的字符,若要表示一个辅助平面内的字符,需要两个char。

来看一个BMP中的字符:”我”

Unicode编码:U+6211
UTF8编码:E68891
UTF16BE编码:FEFF6211
UTF16LE编码:FFFE1162

UTF16BE、UTF16LE分别指的是机器中的大端表示和小端表示,前面的FEFFFFFE编译器会帮我们过滤掉,我们这里只看大端表示。

对于UTF-8表示,先将6211转换为二进制编码110 001000 010001,是15个有效位。对照utf-8编码表,可知需要用三个字节(1110xxxx 10xxxxxxxx 10xxxxxxxx)来表示,将有效位填入,得utf-8编码:11100110 10001000 10010001,转换为16进制,就是E68891

对于UTF-16表示,6211是可以用两个字节表示完的,所以UTF-16的编码就是6621

验证:

String

对于辅助平面内的字符,一个char可不够用了,需要由两个char来存储,或是用String来表示。

来看一个emoji字符:”?”

Unicode编码:U+1F449
UTF8编码:F09F9189
UTF16BE编码:FEFFD83DDC49
UTF16LE编码:FFFE3DD849DC

对于UTF-8表示,先将1F449转换为二进制编码11111 010001 001001,是17个有效位。对照utf-8编码表,可知需要用四个字节(11110xxx 10xxxxxxxx 10xxxxxxxx 10xxxxxx)来表示,将有效位填入,得utf-8编码:11110000 10011111 10010001 10001001,转换为16进制,就是F09F9189

对于UTF-16表示,需要用到代理对进行编码:首先用1F449-10000得到F449,将F449转换为20位二进制0000111101 0001001001,高10位转成十六进制得3D,加上D800D83D,后10位转成十六进制得49,加上DC00DC49,所以它的utf-16编码为D83D DC49

验证:

问题:
String.length()会将一个非bmp中的字符算为1还是算为2呢?
测试↓

可见,如果String中存在非bmp中的字符,String会将它算成两个字符长度。看String.length()的源码也可知道这点:

public int length() {
    return value.length;
}

value是String内部的一个字符数组。

java对码点计算的支持

其实String中也提供了计算码点的方法:String.codePointCount(int beginIndex, int endIndex)

利用的是Character类能判断一个码点是否为代理码点:
Character.isHighSurrogate:是否为高位代理码点

public static boolean isHighSurrogate(char ch) {
    // Help VM constant-fold; MAX_HIGH_SURROGATE + 1 == MIN_LOW_SURROGATE
    return ch >= MIN_HIGH_SURROGATE && ch < (MAX_HIGH_SURROGATE + 1);
}

其中MIN_HIGH_SURROGATE=\uD800MAX_HIGH_SURROGATE=\uDBFF,注明了高位代理码点的值边界。

Character.isLowSurrogate:是否为低位代理码点

public static boolean isLowSurrogate(char ch) {
    return ch >= MIN_LOW_SURROGATE && ch < (MAX_LOW_SURROGATE + 1);
}

其中MIN_HIGH_SURROGATE=\uDC00MAX_HIGH_SURROGATE=\uDFFF,注明了高位代理码点的值边界。

要判断字符串内有没有代理码点,只需确定连续的两个码点一个是高位代理码点一个是低位代理码点就行了。

String.codePointCount依赖的方法Character.codePointCountImpl源码:

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;
}

如果要遍历一个字符串中的字符,可能需要考虑是否有非kmp中的字符的情况。那么就需要用码点为单位来处理:

反例:

正确处理方式:

String.codePoints()得到一个int类型的流,代表这个码点的unicode编码,用System.out.printf()中的%c格式化输出它,就可以看到这个字符。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/%e8%af%a6%e8%a7%a3java%e4%b8%ad%e7%9a%84unicode%e7%bc%96%e7%a0%81%ef%bc%88%e7%a0%81%e7%82%b9%ef%bc%89/

(0)
彭晨涛彭晨涛管理者
上一篇 2019年11月24日
下一篇 2019年11月26日

相关推荐

  • try-catch-finally字节码实例探究

    本文使用Idea的jclasslib插件查看字节码。本文全程自言自语,请勿自行代入。 概述 java是怎么处理try-catch-finally的? 我们在深入理解java虚拟机第…

    2020年4月8日
    0730
  • Java基础查缺补漏01

    某些点会在不远的将来深挖。 >>是逻辑右移,>>>是算术右移 JDK6 可以使用Console.readPassword从控制台中读取密码,用户输入的过程中密码是不可见的。…

    2019年11月26日
    0590
  • Java中的四种内部类

    我发现最近真是越来越没有东西写了。。。不可能天天学习新知识啊,最近在复习阶段了,复习的东西大多数是博客里写过的/(ㄒoㄒ)/ 复习Java基础的时候认真看了一下Java的内部类,这…

    Java 2020年5月23日
    0100
  • 常用开源数据库连接池C3P0、Druid介绍

    概述 很多时候,连接的混乱管理所造成的系统资源开销过大成为制约大型企业级应用效率的瓶颈。因为每一次WEB请求都要建立一次数据库连接,建立连接是一个耗费资源的活动,每次都得花费0.0…

  • 数据库理论之OLTP与OLAP

    OLTP OLTP 即联机事务处理,就是我们经常说的关系数据库,意即记录即时的增、删、改、查,就是我们经常应用的东西,主要供基层人员使用,进行一线业务操作,这是数据库的基础。 对于…

    项目/编程理论 2020年2月16日
    0310
  • 深入理解java虚拟机第三版读书笔记04

    以下是第三章 垃圾收集器与内存分配策略的内容 概述 程序计数器、虚拟机栈、本地方法栈是线程独有的,栈帧更是随方法结束而消亡,不需要垃圾回收。而堆和方法区则需要经过垃圾回收的设计 对…

    2020年1月8日
    0290
  • 深入理解java虚拟机第三版读书笔记03

    续深入理解java虚拟机第三版读书笔记02 HotSpot虚拟机对象探秘 对象的创建 当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一…

    2020年1月6日
    0140
  • AQS及其应用ReentrantLock源码分析

    AQS原理 概述 全称是 AbstractQueuedSynchronizer(抽象同步队列),是阻塞式锁和相关的同步器工具的框架。 特点:+ 用 state 属性来表示资源的状态…

    2020年2月4日
    0120
  • ConcurrentHashMap源码分析

    ConcurrentHashMap的源码比较难读,特别是JDK8中几乎对其进行了重写,设计思想非常优秀,代码量也是非常多,不过我这里算是总结了一些相对重要的部分,仅供参考。 线程安…

    2020年2月13日
    0170
  • CopyOnWriteArrayList源码分析

    总结 总结放前面防止太长不看 CopyOnWriteArrayList是一个线程安全、并且在读操作时无锁的List实现。 CopyOnWriteArrayList内部通过volat…

    Java 2020年2月15日
    0200

发表回复

登录后才能评论