重拾Java(02) - 字符编码

ASCII码

在计算机内部,所有的信息最终都表示为一堆二进制形式的数据。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,称为一个字节(byte),从0000000到11111111。上世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系做了统一规定,称之为ASCII码(American Standard Code for Information Interchange)并沿用至今。ASCII码一共规定了128个字符的编码,比如空格是32(00100000),大写的字母A是65(01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。
  英语用128个符号编码就够了,但是其他语言很多都不止128个符号,比如在法语中,字母上方有注音符号,这种字符就无法用ASCII码表示。为此,在某些欧洲国家会利用字节中闲置的最高位编入新的符号,例如法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是这里又出现了新的问题,不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母ג,但是不管怎样,所有这些编码方式中,0127表示的符号是一样的,不一样的只是128255的这一段。
  至于亚洲国家的文字,使用的符号就更多了,1994年由中华书局、中国友谊出版公司出版的《中华字海》就收录了85568个汉字。一个字节只能表示256种符号肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码GB2312使用两个字节表示一个汉字,所以理论上最多可以表示65536个符号。

Unicode

世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失,这就是Unicode。Unicode是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0041表示英语的大写字母A,U+660A表示汉字"昊"。需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字"昊"的Unicode是十六进制数660A,转换成二进制数是0110 0110 0000 1010,也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节。这里就有两个问题,第一个问题是,如何才能区别Unicode和ASCII?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果Unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。为此出现了Unicode的多种存储方式,也就是说有多种不同的二进制格式可以用来表示Unicode。

UTF-8

UTF-8就是在互联网上使用最广的一种Unicode的实现方式。其他实现方式还包括UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示),不过在互联网上基本不用。UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8的编码规则很简单,只有二条:

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的Unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
  2. 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的Unicode码。
    下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围 UTF-8编码方式
0000 0000-0000 007F 0xxxxxxx
0000 0080-0000 07FF 110xxxxx 10xxxxxx
0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

说明:解读UTF-8编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

Java中的编解码

I/O操作时的编解码

在进行I/O操作时经常会遇到将字节流转换成字符流的场景,Java的API提供了InputStreamReader和OutputStreamWriter来解决这样的问题,而这两个类的构造器中都可以指定编码/解码的方式。

InputStreamReader(InputStream in)  // 使用默认的字符集
InputStreamReader(InputStream in, String charsetName)  throws UnsupportedEncodingException
InputStreamReader(InputStream in, Charset cs)
InputStreamReader(InputStream in, CharsetDecoder dec)
OutputStreamWriter(OutputStream out)  // 使用默认的字符集
OutputStreamWriter(OutputStream out, String charsetName) throws UnsupportedEncodingException
OutputStreamWriter(OutputStream out, Charset cs)
OutputStreamWriter(OutputStream out, CharsetEncoder enc)

从JDK 1.4引入了NIO开始,我们可以使用Charset类提供encode和decode方法实现字符数组和字节数组的转换,代码如下所示:

Charset cs = Charset.forName("utf-8");
String str = "骆昊";
ByteBuffer buffer1 = cs.encode(str);
// 骆                           昊
// e9        aa        86       e6       98       8a
// 11101001  10101010  10000110 11100110 10011000 10001010
for (int index = 0; index < buffer1.limit(); index += 1) {
    System.out.print(Integer.toHexString(buffer1.get(index) & 0xff) + " ");
}
System.out.println();
CharBuffer buffer2 = cs.decode(buffer1);
// 骆昊
System.out.println(buffer2.toString());

字符串的编解码

Java中的String类提供了用字节数组和指定的编码构造字符串对象的操作,同时也提供了将字符串按照指定的编码解码成字节数组的操作,下面我们来做几个小实验。

实验1:中文变成'?'。

public static void main(String[] args) throws UnsupportedEncodingException {
    String str = "hello, 骆昊";
    byte[] buffer = str.getBytes("iso-8859-1");
    // hello, ??
    System.out.println(new String(buffer));
}

说明:ISO-8859-1是单字节编码,中文“骆昊”的编码(0x9a86和0x660a)会被转换成0x3f,而0x3f是ASCII码中的'?',所以中文就变成了问号,而且中文字符的编码信息已经丢失,再怎么解码也没有机会还原出原来的中文字符了。所以这种现象也称之为“编码黑洞”,因为它把不认识的字符给吞噬掉了。很多Java的框架和产品默认都使用了ISO-8859-1,所以这个问题很常见。

实验2:中文变成看不懂的字符。

public static void main(String[] args) throws UnsupportedEncodingException {
    String str = "hello, 骆昊";
    byte[] buffer = str.getBytes("gbk");
    // hello, Âæê»
    System.out.println(new String(buffer, "iso-8859-1"));   
}

说明:这种情况在使用浏览器的时候也很常见,服务器传过来的是中文字符但是浏览器的编码却设置为ISO-8859-1就会出这种问题。

如果中文经过了多次编解码,那么还有可能遇到一个中文字符变成多个问号的情况。其实要解决这些编码问题原则非常简单,首先如果要表示中文字符就不能使用单字节编码,这样势必会出现“黑洞”;其次编码和解码使用的“码”应当是一致的。

URL编码

URL是统一资源定位符(Universal Resource Locator)的缩写,是Internet上标准的资源地址。它最初是由万维网和浏览器的发明者英国人Tim Berners-Lee发明用来作为万维网的地址,现在已经被W3C编制为Internet标准(RFC 1738)。统一资源定位符的标准格式如下:

协议://服务器域名或地址:[端口号]/资源路径/文件名[?查询参数]

我们试一试在用谷歌搜索“骆昊”,来看看浏览器地址栏中的URL到底是什么样的。

https://www.google.com.hk/#safe=strict&q=%E9%AA%86%E6%98%8A

URL中允许出现的字符分为保留字符(有特殊含义的字符)与未保留字符,未保留字符包括英文大小写字母、0-9的数字以及‘-’、 ‘_’、 ‘.’和'~',保留字符包括 ‘!’、 ‘*’、 ‘'’、 ‘(’、 ‘)’、 ‘;’、 ‘:’、 ‘@’、 ‘&’、 ‘=’、 ‘+’、 ‘$’、 ‘,’、 ‘/’、 ‘?’、 ‘#’、 ‘[’和‘]’。如果URL中需要用到保留字符或者非URL允许的字符则需要使用百分号编码,例如:‘=’要处理成‘%3D’、‘+’要处理成‘%2B’、而上面要搜索的‘骆’和‘昊’两个中文字符被处理成了百分号编码的‘%E9%AA%86’和‘%E6%98%8A’。

Java中要将URL中的非URL允许字符处理成百分号编码有非常简单的办法,就是使用URLEncoder类的encode方法,代码如下所示。

public static void main(String[] args) throws UnsupportedEncodingException {
    String urlStr = "Java 骆昊";
    String encodedUrlStr = URLEncoder.encode(urlStr, "utf-8");
    // Java+%E9%AA%86%E6%98%8A
    System.out.println(encodedUrlStr);
}

几种编码格式的比较

表示中文可以选择的编码方式很多,包括GB2312、GBK、GB18030、UTF-8和UTF-16。UTF-16定义了Unicode字符在计算机中的存取方式,用固定长度的两个字节来表示所有的字符,Java中的char类型之所以是两个字节就是因为Java使用了UTF-16作为内存中字符存储的格式。UTF-16的编码效率高,字符与字节之间的转换也相对简单,但是如果在网络上传输数据的话会遇到大尾数和小尾数字节顺序转换的问题,因此UTF-8更适合在网络上传输数据,而UTF-16更适合在内存中使用。UTF-8使用了变长存储的方式,对ASCII字符采用单字节存储,对其他字符可以使用1~6个字节来表示,编码效率介于GBK和UTF-16之间,因此开发Java Web应用时,强烈建议使用UTF-8这种编码方式。

推荐阅读

  1. ASCII - https://zh.wikipedia.org/wiki/ASCII
  2. Unicode - https://zh.wikipedia.org/wiki/Unicode
  3. UTF-8 - https://zh.wikipedia.org/wiki/UTF-8
  4. 中文编码 - https://zh.wikipedia.org/wiki/%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 字符集和编码简介 在编程中常常可以见到各种字符集和编码,包括ASCII,MBCS,Unicode等字符集。确切的说...
    兰山小亭阅读 8,453评论 0 13
  • 大概每个人在使用软件时都遇到过乱码的问题,这是由于字符的编码和解码方式不一致导致,我们知道计算机只认识二进制数据,...
    楚客阅读 1,416评论 1 9
  • 每每看到好看的花朵,都是十分喜欢,于是也有了要养一颗的想法。打听后,打消了念头,还是养盆绿叶的吧。 只需浇水,没有...
    蜜觅奇点阅读 426评论 0 1
  • 1. 浮动元素有什么特征?对父容器、其他浮动元素、普通元素、文字分别有什么影响? 浮动元素特征: 浮动元素会脱离正...
    billa_8f6b阅读 218评论 0 0
  • 有一种守候静默无言我以为你会明白那里有我深藏的热情如冬夜的炉火而你怎能视而不见 有一种等待含蓄矜持我多希望你会明白...
    红尘久客阅读 820评论 16 11