Java程序员应该懂的字符编码知识

前言:本文整理了Unicode和UTF编码的关系,UTF-8,UTF-16和UTF-32 的区别以及Java字符汉字占用字节数和MySQL中varchar长度等常见字符问题,相关内容较繁琐,故作此记录.文章内容较多,希望对你有所帮助.

目录:

1 概念定义

  • 字符(Character):
    各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。
  • 字符集(Character set)
    多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。
  • Unicode
    Unicode是为整合全世界的所有语言文字而诞生的。任何字符在Unicode中都对应一个值。这个值就可以称为这个字符的Unicode值。Unicode的值通常写成 U+ABCD 的格式。
  • UTF
    UnicodeTransformationFormat的缩写,意为Unicode转换格式,即把Unicode的值转做某种格式的意思。
  • BMP
    基本多文种平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0),是Unicode中的一个编码区段。编码从U+0000至U+FFFF

2 Unicode和UTF的关系

  • UTF-32、UTF-16和 UTF-8 是 Unicode 标准的编码字符集的字符编码方案:
    Unicode本身作为编码字符集没有任何存储形式,只是一个编号和字符对应的表而已,而UTF则规定了具体用一个怎么样的二进制数值来将一个字符的整数编号存储到计算机内.
  • 例子
    “文章采用的是utf-8编码方式”,更准确的说法应该是“文章采用的是基于unicode编码字符集的utf-8的编码方案”
  • 注意
    1. 很多时候把gbk和utf-8放一起,但其实这里的utf-8指的是utf-8的Unicode字符集
    2. 有时候把Unicode和utf-8放一起,但其实这里的Unicode是utf-16的编码方案

3 UTF-8 转换表

位数(bit数) UTF-8表示 字节数(byte数)
0~7 0XXX XXXX 1
8~11 110X XXXX,10XX XXXX 2
12~16 1110XXXX,10XX XXXX,10XX XXXX 3
17~21 1111 0XXX,10XX XXXX,10XX XXXX,10XX XXXX 4
22~26 1111 10XX,10XX XXXX,10XX XXXX,10XX XXXX,10XX XXXX 5
27~31 1111 110X,10XX XXXX,10XX XXXX,10XX XXXX,10XX XXXX,10XX XXXX 6

由上表可知:

  • 实际表示ASCII字符的UNICODE字符,将会编码成1个字节,并且UTF-8表示与ASCII字符表示是一样的。
  • 所有其他的UNICODE字符转化成UTF-8将需要至少2个字节。
  • 每个字节由一个换码序列开始。第一个字节由唯一的换码序列,由n位连续的1加一位0组成, 首字节连续的1的个数表示字符编码所需的字节数。

4 UTF-8,UTF-16,UTF-32 的区别

编码方案 特点 长度 优点 英文字符所占长度 汉字所占长度
UTF-8 可变长度 1~6字节 网络传输节省带宽 一个字节 绝大多数汉字占用三个字节,个别汉字占用四个字节
UTF-16 有限可变长度(1或2个16位,一开始1个,后来支持2个) 2或4字节 容错机制好(位置固定,所以中间出错不会影响到后面) 两个字节 绝大多数汉字(尤其是常用汉字)占用两个字节,个别汉字(在后期加入unicode编码的汉字,一般是极少用到的生僻字)占用四个字节
UTF-32 定长(32位) 4字节 没有 4字节 4字节

注意:

  • 不同位数影响的主要是带宽而不是存储容量,如果用UTF-8一个字节就能传的使用UTF-16两个字节来传,就相当于传输时间翻倍或带宽减半.
  • 本地存储一般会使用更节省空间的字符集,如GBK(ASCII一个字节,汉字大部分两个字节)
  • utf-16一开始是定长16位即2个字节长的,后拓展到可以使用2个16位,但可能未广泛支持
  • 是先有UTF-16再有UTF-8,由于英文主要使用ASCII字符,所占只需7位,,而UTF-16每个字符至少占16位,所以对于英文世界来说使用UTF-16会产生巨大浪费,这一点在网络时代更加明显.所以才有了UTF-8变长字符编码,但需要使用前缀换码序列.由于汉字在UTF-16中本来就需要两个字节(16位),加上前缀后就需要使用3个字节了,可以参考上文的UTF-8转换表.所以,有人会说,中文适合用UTF-16,英文适合用UTF-8.

5 Java的内码和外码

  • 内码( internal encoding ):
    内存中的编码方式
  • 外码( external encoding):
    除了内码都是外码

注意:

  • Java的class文件采用UTF8来存储字符,也就是说,class中字符占1~6个字节。
  • Java**序列化**时,字符也采用UTF8编码,占1~6个字符。

6 Java字符和char所占字节数 及 Java汉字长度

之所以要区分开字符和char的概念是因为单独讲char没什么意义,看下表

基础类型 字节数 位数
byte 1 8
char 2 16
short 2 16
int 4 32
float 4 32
double 8 64
long 8 64

和C或C++根据平台类型长度可变不同,Java有虚拟机,虚拟机规范就这么规定了,想都不用想,char就占两个字节共16位,问题结束

但我们真正感兴趣的是一个Unicode字符所占的字节数

首先需要明确Java使用哪种编码字符集和编码方案.

Java编码字符集毋庸置疑是Unicode,编码方案则复杂一些,内码使用UTF-16,外码使用UTF-8,没有特别说明,一般指内码.所以会说Java采用UTF-16编码.

早期,UTF16采用固定长度2字节的方式编码,两个字节可以表示65536种符号(其实真正能表示要比这个少),足以表示当时unicode中所有字符。但是随着unicode中字符的增加,2个字节无法表示所有的字符,UTF16采用了2字节或4字节的方式来完成编码。Java为应对这种情况,考虑到向前兼容的要求,Java用一对char来表示那些需要4字节的字符。所以,java中的char是占用两个字节,只不过有些字符需要两个char来表示。如何用两个char表示一个Unicode字符请看下文

而前文也说了,绝大部分的汉字在UTF-16中占两字节,即在Java中,绝大部分的汉字可用一个char表示.

7 Java用两个char表示一个Unicode字符的方法

首先先看一下Java String的构造方法
String有三类构造方法,一类是传入byte[],一类是传入char[],一类是传入int[],如下:

public String(byte bytes[], int offset, int length);//字节流,转成字符串
public String(char value[], int offset, int count);//字符流,转成字符串。较少用,一般用在双字节编码的字节串转String。
public String(int[] codePoints, int offset, int count);//代码点,转成字符串

String内部数据存储使用的是char[]存储,所以byte[]参数的构造函数会把byte[]解析成正确的代码点,也就是int类型,再转换成char[]类型存到String内部。

所以,我们这里只分析int[]类型入参的构造函数,来看一下String是如何把int类型的代码点转成char[]存起来的。从这里,也能看出来一个2个字节的Unicode和一个2个以上字节的Unicode代码点是怎么存到String里的。

注意:BMP是Unicode中的一个编码区段。编码从U+0000至U+FFFF,即占两个字节(16位),可以理解为一个Java(utf16)的char刚好能表示BMP的范围的字符

    public String(int[] codePoints, int offset, int count) {
        //....
        // Pass 1: 计算int[]转成char[]后的长度
        int n = count;
        for (int i = offset; i < end; i++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))//如果是BMP范围的代码点,那么计数加1
                continue;
            else if (Character.isValidCodePoint(c))//如果是超出BMP范围的有效代码点,那么计数加2
                n++;
            else throw new IllegalArgumentException(Integer.toString(c));
        }

        // Pass 2: 使用int[]的值填充char[]
        final char[] v = new char[n];

        for (int i = offset, j = 0; i < end; i++, j++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))//如果是BMP范围的代码点,直接加入char[]
                v[j] = (char)c;
            else//如果是超出BMP范围的有效代码点,那么转成2个char,存入char[]
                Character.toSurrogates(c, v, j++);
        }

        this.value = v;
    }

8 Java判断字符串是否包含中文的方法

方法一:

//代码来自HanLP自然语言处理库,git地址:https://github.com/hankcs/HanLP/blob/master/src/main/java/com/hankcs/hanlp/utility/TextUtility.java
/**
 * 判断某个字符是否为汉字
 *
 * @param c 需要判断的字符
 * @return 是汉字返回true,否则返回false
 */
public static boolean isChinese(char c)
{
    String regex = "[\\u4e00-\\u9fa5]";
    return String.valueOf(c).matches(regex);
}

方法二:

//来源地址:https://blog.csdn.net/z69183787/article/details/53162069这里考虑进了CJK的扩展字符集


    // GENERAL_PUNCTUATION 判断中文的“号  
    // CJK_SYMBOLS_AND_PUNCTUATION 判断中文的。号  
    // HALFWIDTH_AND_FULLWIDTH_FORMS 判断中文的,号  
    private static final boolean isChinese(char c) {  
        Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);  
        if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS  
                || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS  
                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A  
                || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION  
                || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION  
                || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS) {  
            return true;  
        }  
        return false;  
    }

一般来说用第一种方法就足够了,扩展字符集用的比较少

9 MySQL的VARCHAR(N)的问题

首先请注意,mysql中各种字符集编码的特征和我们上面了解到的有些不太一样,具体如下

编码方式 支持范围 所需存储字节数
utf8mb3, utf8 BMP only 1, 2, or 3 bytes
ucs2 BMP only 2 bytes
utf8mb4 BMP and supplementary 1, 2, 3, or 4 bytes
utf16 BMP and supplementary 2 or 4 bytes
utf16le BMP and supplementary 2 or 4 bytes
utf32 BMP and supplementary 4 bytes

如上,MySQL的UTF-8和Java里的UTF-8不一样,MySQL的实现里utf8最大只允许存储3个字节,所以要注意,即使同样适用UTF-8编码,在Java中可以表示的在MySQL中就可能存不下了(比方说 emoji表情,参考https://blog.csdn.net/alinyua/article/details/79599540),这时候就需要适用 utf8mb4了

而VARCHAR(N)的N值不论是哪种编码,都对应到了字符级别,也就是这个列可以存下N个字符.

MySQL中VARCHAR(N)字段类型里边的N代表最大可以存多少个字。如果选的是中文编码,比如utf8编码,那么N代表最大可以存多少个汉字。

N的最大值主要限制因素是MySQL的最大行大小,MySQL的行大小最大不能超过65535bytes,是说MySQL的一行里所有列占用字节数的和不能超过65535bytes。BLOB和TEXT比较特别,他们只占用9个字节大小,真实数据存储在行外。也就是说,如果选用的是utf8mb4编码,N的最大值会比适用utf8编码的小.

参考资料:
https://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html
http://swiftlet.net/archives/category/char-encoding
http://www.cnblogs.com/yinhaichao/p/4090802.html
https://blog.csdn.net/BuquTianya/article/details/80699202
http://www.importnew.com/29073.html
http://www.cnblogs.com/louiswong/p/6062417.html

猜你喜欢

转载自blog.csdn.net/alinyua/article/details/80961342