大数跨境
0
0

字符编码简介

字符编码简介 点融黑帮
2018-12-06
2
导读:介绍了字符集和编码的基础知识以及java平台内部的编码机制

1.基础概念

字符集合(character set)

人类语言中使用的字符的一个集合,可以理解为一个字库

编码字符集(coded character set)

给字符集合中的每个字符一个唯一的编号(code point),从而形成一个编号对应一个字符的集合,如下图Unicode的编码字符集的平面图:

字符编码(character encoding)

将人类语言中字符映射为计算机可进行存储和操作的字节序列,常用的编码格式有ASCII、ISO-8859-1、UTF-8等,如下图所示为:

举个例子来说,在XML文件中定义字符编码为UTF-8编码格式:

<?xml version="1.0" encoding="UTF-8"?>
  • 字符集(charset)

    由于历史原因character set在很多地方有着不同的含义,所以提出了charset这个概念,charset与character set是不同的概念,所以不能混淆。通常意义上我们所说的charset是与字符编码(character encoding)相同意思。代表着从字符转换为字节序列的方式。

HISTORICAL NOTE: The term "character set" was originally used in MIME to describe such straightforward schemes as US-ASCII and ISO-8859-1 which consist of a small set of characters and a simple one-to-one mapping from single octets to single characters. Multi-octet character encoding schemes and switching techniques make the situation much more complex. As such, the definition of this term was revised to emphasize both the conversion aspect of the process, and the term itself has been changed to "charset" to emphasize that it is not, after all, just a set of characters. A discussion of these issues as well as specification of standard terminology for use in the IETF appears in RFC 2130.[1]

举个例子来说:

1.Java程序中也使用Charset来表示字符编码,例如申明一种UTF-8的字符集:

public static final Charset UTF_8 = Charset.forName("UTF-8");

2.在HTTP头中使用charset来指明HTTP协议的编码格式

Content-Type: text/html; charset=utf-8

以上各个名词的细节不用太过于纠结,过于纠结容易陷入文字游戏的陷阱,所以大体上能知道各个名词的意义即可。

2.Unicode与UTF

Unicode.org上为unicode的介绍是给每个字符提供一个跨平台、跨程序、跨语言的唯一编码。

Unicode provides a unique number for every character, no matter what the platform, no matter what the program, no matter what the language.[3]

在Unicode中这个编码(后面使用码点作为描述)的格式为U+[XX]XXXX,X代表一个十六制数字,码点的位数是4-6位但常用字符基本是4位。比如点融这两个字用Unicode的码点来表示即为:U+70B9,U+878D。Unicode将所有的码点按照65536的大小分为了17个平面,第一个平面(Basic Multilingual Plane)的码点范围是从U+0000~U+FFFF,剩余的16个平面被称为是增补平面(Supplementary planes)。

那UTF又是什么呢?UTF即是Unicode Transformation Format,是专门针对Unicode进行转换的一种编码方式,UTF中又存在UTF-8、UTF-16、UTF-32等编码方式,其中8、16、32分别是指一个字符单元(code unit)的位数,比如UTF-8也就是8位一个字符单元,但是要注意UTF-8和UTF-16都是变长的编码方式,以UTF-8进行编码的字符可能存在1-4个字节数的组合,而UTF-16则会存在2或者4个字节数。下面简单的介绍一下UTF-8编码的规则:

1.若一个字节的第8位(最高)为0,则表示这个是一个ASCII的字符,由此可见UTF-8是兼容了ASCII的编码方式的。

2.若一个字节以多个1开头,连续的1个数则暗示着这个字符所需的字节数。

3.若一个字节以10开头,则表示不是首字节,还需要向前才能查询到当前字符的首字节。

下图表示了分别占1-4个字节UTF-8的编码规则:

给大家举个例子看看如何从unicode的码点转换成UTF-8,比如说“点”字的码点位U+70B9:

 
 
 
  1.                      U+70B9


  2. 二进制:                0111 0000 1011 1001


  3. 按照4+6+6进行分组:      0111 000010 111001


  4. 使用UTF-8规则:         11100111 10000010 10111001


  5. 转换为16进制:          E7 82 B9


大家可自行对尝试对“融”字进行UTF-8编码转换。

UTF-16点编码方式对于在unicode第一平面(BMP)的内的字符来说,是无需转换的,比如“点”字的UTF-16编码也就是它的Unicode的码点70B9,但是对于在剩下的增补平面转换规则就稍微复杂。由于篇幅原因Unicode和UTF相关的内容就先介绍这么多,需要深入了解的同学可自行查询相关资料。

3.Java中的编码

以上花了这么多篇幅来介绍编码的基础知识可能很多同学会说了解这些有什么用呢?在我们实际的编码过程中也没看到有什么地方需要用到这些东西,接下来我们就结合Java语言来实际看看编码的应用。

3.1 Java的内码

不知道大家以前在写Java代码的时候会不会对我们最常用的String类产生疑问,例如:

 
 
 
  1. String s = "中文";


这段代码在执行的时候,“中文”到底是以什么形式存在在java平台内部中的呢,带着这个疑问我们来深入一下String类的源代码,

 
 
 
  1. public final class String


  2.    implements java.io.Serializable, Comparable<String>, CharSequence {


  3.    /** The value is used for character storage. */


  4.    private final char value[];


  5. ......


其实大家都知道在String中是使用一个final的char数组来存储具体的字符,但是这个具体每个char又是怎么来表示的呢?接下来我们深入一下Character类中的Javadoc,我选取了其中比较重要的部分截取给大家。

 
 
 
  1. The Java platform uses the UTF-16 representation in char arrays and in the


  2. String and StringBuffer classes. In this representation, supplementary


  3. characters are represented as a pair of char values, the first from the


  4. high-surrogates range, (\uD800-\uDBFF), the second from the low-surrogates


  5. range (\uDC00-\uDFFF).


这个时候大家可能会恍然大悟原来Java平台是使用的UTF-16编码来作为系统的编码用于在char或者String等中表示字符。但是这个时候又有一个问题是不是所有的unicode字符都可以用单个char来存储呢,上面这段话的后一句给了我们说明,增补平面的字符是需要一对char来表示,第一个是来自高代理区,第二个来自低代理区。我们来做一个实验看一下是否能够验证这段话。

由上代码片段可知在char中表示一个UTF16代码单元的字符是不会有任何问题的,但是表示😀(Unicode U+1F600)字符时出现了编译错误。这个是因为😀按照UTF-16编码后为(\uD83D\uDE00)是需要2个字符单元的,故使用一个char去表示两个字符单元的字符时就肯定会编译错误了。

在了解了JVM内部中的字符是怎么表示之后,我们来看一些平时我们使用较多,但是可能很少会关注内部实现的一些方法。

  • String.length()方法返回的是字符单元数(The length is equal to the number of Unicode code units in the string),所以如果是在增补平面的字符返回的长度就是2。

  • String.charAt(int index)方法是返回传入index对应的char的值,若String中仅有一个增补平面的字符,那么String.charAt(0)是无法返回正确的可识别的字符的。

所以在处理字符的时候若含有增补平面内的字符时,需特别注意以上各个方法的具体返回。

3.2 Java中的编解码

说了这么多java平台内部处理字符的逻辑,我们再来看一下我们写代码中最常用的编码解码的场景。

 
 
 
  1. /**


  1.    public String(byte bytes[], String charsetName)


  2.            throws UnsupportedEncodingException {


  3.        this(bytes, 0, bytes.length, charsetName);


  4.    }



  1.    public byte[] getBytes(String charsetName)


  2.            throws UnsupportedEncodingException {


  3.        if (charsetName == null) throw new NullPointerException();


  4.        return StringCoding.encode(charsetName, value, 0, value.length);


  1.    }


第一个方法是将字节数组以指定字符编码格式进行decode成一个String字符串的方法。第二个方法则是将一个String字符串以指定的字符编码格式进行编码成一个字节数组。仔细查看源代码会发现这两个方法会调用到的CharsetDecoder和CharsetEncoder这两个抽象类来进行编解码,最终实际上还是会使用到具体的字符编码集内部的Encoder和Decoder的方法,要么是通过码表进行转换,要么是通过某种算法进行转换。编码的时序图如下所示:

另外比较特殊的一点是若不指定具体的编码字符集的情况下,则使用defaultCharset,而defaultCharset由系统属性file.encoding 决定,如果用户没有设置JVM的这个属性,其值依赖于启动该JVM的环境编码:比如是由操作系统的编码;或者是在IDEA或Eclipse里面启动JVM,可以设置JVM的这个属性,默认情况下IDEA的file.encoding是UTF-8。所以在使用编码或者解码的方法时必须要指定对应的编码格式,不然程序在各种环境下的表现情况可能存在不一致。

以上内容只是简单的介绍了字符编码与java相关的知识点,并没有太多结合实际的场景来解决比如说乱码等问题,有兴趣的同学可以深入研究一下从Web页面到服务端后台到数据库整个过程中的编码转换过程,或者期待下一遍文章。


[1]https://www.ietf.org/rfc/rfc2278.txt

[2]https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.4

[3]http://www.unicode.org/standard/WhatIsUnicode.html

[4]https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html?mhq=JAVA%20%E4%B9%B1%E7%A0%81&mhsrc=ibmsearch_a


往期精彩:







【声明】内容源于网络
0
0
点融黑帮
点融黑帮——一个充满激情和梦想的技术团队,吸引了来自金融及信息科技领域的顶尖人才。我们正在用技术创新改变传统金融。
内容 374
粉丝 0
点融黑帮 点融黑帮——一个充满激情和梦想的技术团队,吸引了来自金融及信息科技领域的顶尖人才。我们正在用技术创新改变传统金融。
总阅读314
粉丝0
内容374