详解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日

相关推荐

  • Java基础查缺补漏04

    继续我的复习刷题 接口方法可以使用abstract修饰 问题: java接口的方法修饰符可以为?(忽略内部接口) A. privateB. protectedC. finalD. …

    Java 2020年5月28日
    0200
  • 深入理解java虚拟机第三版读书笔记07

    附: Java虚拟机规范-Java虚拟机指令集:JDK8 Java虚拟机规范-Java虚拟机指令集:JDK13 续深入理解java虚拟机第三版读书笔记06 字节码指令简介 Java…

    Java 2020年1月21日
    0230
  • Collection接口研究

    以下内容基于jdk1.8 接口Collection分析 img 该接口实现了接口Iterable 方法: int size(); 返回元素的个数 boolean isEmpty()…

    2019年11月13日
    0100
  • ConcurrentHashMap源码分析

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

    2020年2月13日
    0160
  • Java中的四种内部类

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

    Java 2020年5月23日
    0100
  • 快速失败(fail-fast)和安全失败(fail-safe)

    快速失败 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。…

    Java 2020年2月23日
    0140
  • 为什么说Java只有值传递?

    先说一下。。以后可能不会怎么写Java相关的博客了,因为找到了字节跳动的实习工作,用Go/Python开发后端,所以这几天在抓紧时间学Go,在学Go的时候,了解到Go语言只有值传递…

    Java 2020年6月26日
    03100
  • 深入理解java虚拟机第三版读书笔记03

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

    2020年1月6日
    0130
  • NIO底层原理-epoll

    BIO模型存在三个socket: ServerSocket:专门用来监听是否有来自客户端的连接accept返回的Socket:专门用于处理客户端请求的socketSocket:客户…

    Java 2020年2月13日
    0110
  • Java基础查缺补漏02

    哈哈我其实没有想到这个系列真会有续集,上次写完01以后以为不会再写下去了,没想到最近牛客网刷题有些题目还是挺纠结的,这里补一补 构造器能带哪些修饰符 题目: Which of th…

    Java 2020年5月25日
    080

发表回复

登录后才能评论