`

转:java字符集的欺骗性

 
阅读更多
http://www.csdn123.com/html/blogs/20130419/4302.htm
java字符集的欺骗性

返回脚本百事通
"多少年来,乱码问题一直是程序员心中的痛。

多少程序员,在乱码中迷乱却不见字符集的偷笑。"



----------------------------------------------------------------------------------





今天一同事突然跑来找我,说我们之间的线上系统接口突然返回消息出现中文乱码了。

在此之前接口一直正常,但是代码在这期间我们都承认并没有作任何改动。

既然都说程序代码没有改动,那么我首先推测的必然是环境发生了改变。

咨询对方怎么处理接口返回的消息的,他们说进行了字符串字符集编码转换,于是让其粘贴代码来看看。



new String(msg.getBytes(), Charset.forName("GBK"));



于是看到了上面这段似曾相识的代码片段,这段代码是什么含义呢?

有人可能会说:“就是把 msg 字符串从其默认编码转换成 GBK 编码”。

这样想已经不是理解偏差,而是南辕北辙,大相迳庭了。





上面那行代码用到了 String 类的两个 api:



public byte[] getBytes()

public String(byte bytes[], Charset charset)



String 类说是 java 中使用最频繁,最常用的类,应该毫无争议吧。

但可能很多人并没仔细看过这个最熟悉类的java doc。

getBytes()的 java doc 如下描述:



Encodes this String into a sequence of bytes using the platform's default charset, storing the result into a new byte array.



意思是将该字符串使用平台默认字符集编码为一个字节序列并存储于返回的字节数组中。

而另一个构造函数 api 的 java doc 则说:



Constructs a new String by decoding the specified array of bytes using the specified charset.



意思是使用指定的字符集解码字节数组来构造一个新的字符串。



通过 java doc 的说明,我们明白了这个构造函数指定的字符集是用于解码第一个传入参数字节数组的,而不是表达用这个字符集构造一个新的字符串(噢哦,不知道还有多少人被这个构造函数的签名欺骗误导了)。

那么上面的那行代码其实在表达什么意思呢,这里就呼之欲出了。

它将 msg 字符串采用平台默认编码为字节数组,再基于此字节数组通过 GBK 进行解码来构造一个新的字符串。

那么这个过程中发生了什么事情,字符串编码如我们所想从平台默认变为 GBK?如果这样想就真是很傻很天真了。

其实上面代码的执行过程,如果平台默认编码就是 GBK,那么这个代码就是创建了一个新字符串和原来 msg 相等,没副作用,干点无用功。

前文提及的接口乱码的问题正是因为,原先对方平台编码为 GBK,昨天突然改变为 UTF-8 后导致的乱码。





而程序原作者期待的转换编码过程其实完全没有必要,因为 java 中字符串的编码统一采用了 unicode 字符集。

而 String 对象的构造过程,就是将指定字符集编码的字节数组转换为 unicode 编码的字符串。

在 java 中说什么 GBK 编码的字符串,ISO-8859-1 编码的字符串都是无稽之谈。

那么我们经常在 web 开发中使用的一种转码方式,也是一段熟悉的代码可能又会使某些人陷入困惑了,如下:



new String(request.getParameter("xxxx").getBytes("iso-8859-1"),"utf-8")



上面这段代码在基于 tomcat 开发的一些 web 应用中进行转码有效,这又是为什么?

其实原因是从浏览器传入的请求参数一般默认使用的 utf-8 编码,而 tomcat 接收到请求后将参数采用默认的 iso-8859-1 解码生成了一个错误的字符串参数。

要恢复就得先把字符串恢复为原始字节数组,再通过 utf-8 来解码生成正确得字符串,所以才有如上写法。


而如上写法,对于不明白浑浑噩噩的程序员,就误认为将 iso-8859-1 的字符串转换为了 utf-8 格式的字符串。(这也没什么好羞耻的,我也这样以为了好些年:)





但字符集编码转换可不是负负得正那么简单,我们之所以能正确的转换回来,是因为 iso-8859-1 的字符集是单字节字符集编码的。

下面举个简单例子来说明下字符集编码转换的问题。


// 单字节编码字符集
Charset iso88591 = Charset.forName("iso-8859-1");
// 双字节编码字符集
Charset big5 = Charset.forName("big5");
// 可变长度编码字符集
Charset utf8 = Charset.forName("utf-8");


// 原始字符串
String src = "测试";               
// utf8 编码的字节数组
byte[] utf8Bytes = src.getBytes(utf8);   
// 使用 iso-8859-1 错误解码的字符串(乱码)
String wrongStr = new String(utf8Bytes, iso88591);  
// 使用 big5 错误解码的字符串(还是乱码)
String wrongStr2 = new String(utf8Bytes, big5);


System.out.println("wrongStr-iso88591-decoding = " + wrongStr + "    len=" + wrongStr.length());
System.out.println("wrongStr-big5-decoding     = " + wrongStr2 + "   len=" + wrongStr2.length());
System.out.println("orignal-utf8-bytes         = " + Arrays.toString(utf8Bytes));


// 把 iso-8859-1 错误解码的字符串恢复utf8编码的字节数组 - 可逆
byte[] resumeBytes = wrongStr.getBytes(iso88591);  
String rightStr = new String(resumeBytes, utf8);


// 把 big5 错误解码的字符串恢复utf8编码的字节数组 - 不可逆
byte[] resumeBytes2 = wrongStr2.getBytes(big5);
String rightStr2 = new String(resumeBytes2, utf8);


System.out.println("resume-iso88591-utf8-bytes = " + Arrays.toString(resumeBytes));
System.out.println("resume-big5-utf8-bytes     = " + Arrays.toString(resumeBytes2));
System.out.println(rightStr);
System.out.println(rightStr2);



原始字符串 src,首先编码为 utf-8 的字节数组。

用 iso-8859-1 单字节字符集解码得到一个错误的字符串 wrongStr。

用 big5 双字节字符集解码得到一个错误的字符串 wrongStr2。

对两个错误的(乱码)字符串调用 getBytes(charset) 尝试还原为原始字节数组,再使用正确的编码 utf-8 解码生成正确的字符串。

可以看到如下输出结果:



wrongStr-iso88591-decoding = ??è?   len=6
wrongStr-big5-decoding     = 瘚??   len=3
orignal-utf8-bytes         = [-26, -75, -117, -24, -81, -107]
resume-iso88591-utf8-bytes = [-26, -75, -117, -24, -81, -107]
resume-big5-utf8-bytes     = [-26, -75, 63, 63]
测试
???



中文 “测试” 两个字,采用 utf-8 编码,这两个字正好每个编码为 3 个字节,一共6个字节。

iso-8859-1 属于单字节字符集,解码生成的字符串字符和字节一一对应,因此解码后的乱码字符串 wrongStr 长度为 6。

而 big5 属于双字节字符集,解码生成的字符串每 2 个字节对应一个字符,因此解码后的乱码字符串 wrongStr2 长度为 3。

如上,[-26, -75] 字节组合解码为 big5 字符[瘚],而字节组合 [-117, -24] 和 [-81, -107] 在 big5 中没有对应的字符,因此被解码为[??]。

当从一个大字符集字节序列解码为一个小字符集字符串时,可能会产生解码丢失,并且不可逆转,因此如上随后我们用 big5 在编码回来时已经不是原来的字节序列了。





最后,我们在处理 java 编码问题时要区分清楚几个概念

1. java 字符串,内部统一采用 unicode 字符集

2. java 字符串之间不存在字符集转换,只有字节序列之间才存在字符集转换



补充说明:

unicode 是一种符号集,规定了符号的二进制代码,却没规定如何存储,现在的规模容纳了100多万种符号

unicode 的不同存储方式,代表了 unicode 的多种实现方式。

utf-8 只是 unicode 的一种实现方式,类似的还有 utf-16, utf-32

java 的字符串 和 jvm 运行时都是采用 utf-16 格式。


分享到:
评论

相关推荐

    java.sql.SQLException: 不支持的字符集 (在类路径中添加 orai18n.jar): ZHS16GBK

    java.sql.SQLException: 不支持的字符集 (在类路径中添加 orai18n.jar): ZHS16GBK ……

    Java字符集和编码

    ### Java字符集和编码 #### 一、引言 在探讨Java字符集和编码之前,我们先了解一下为什么在Java编程中需要关注字符集和编码。Java作为一种广泛应用的编程语言,其内部采用的是Unicode编码,这使得Java能够很好地...

    java支持的字符集

    ### Java支持的字符集 Java作为一种广泛使用的编程语言,在处理多语言环境下的文本时,其对字符集的支持显得尤为重要。...开发者可以根据实际需求选择合适的字符集进行字符串处理,确保数据的正确性和完整性。

    java字符串的各种编码转换

    在Java中,处理不同字符集之间的字符串转换是一项常见任务。尤其是在处理国际化应用时,理解并掌握各种字符编码格式变得尤为重要。下面将介绍几种常见的字符编码格式以及如何在Java中实现它们之间的转换。 #### 1. ...

    java 字符串转16进制 16进制转字符串 将两个ASCII字符合成一个字节;

    java 字符串转16进制 16进制转字符串 将两个ASCII字符合成一个字节; java 字符串转16进制 16进制转字符串 将两个ASCII字符合成一个字节; java 字符串转16进制 16进制转字符串 将两个ASCII字符合成一个字节; java ...

    java 字符集编码转换,时间格式化,数字判断等,java文件

    java 字符集编码转换,时间格式化,数字判断等,java文件

    Java字符集编码简记

    在IT行业中,字符集编码是基础且至关重要的...总之,理解并掌握Java中的字符集编码,不仅有助于编写出兼容性更强的代码,也是提升软件质量的关键。在日常开发中,应时刻注意编码的选择和转换,以确保信息的准确传递。

    JAVA及相关字符集编码问题

    JAVA及相关字符集编码问题 在深入探讨JAVA与字符集编码问题之前,我们首先需要理解不同字符集编码的基本概念以及它们在JAVA环境中的应用。字符集编码是计算机系统中表示文字的一种方式,它决定了如何将字符转换为二...

    java字符集编码问题

    ### Java字符集编码问题详解 #### 一、引言 在Java编程中,字符集编码问题是一个常见且重要的议题。由于不同的系统、平台以及网络环境中可能存在多种字符编码格式,这导致了在处理文本数据时可能会遇到编码不一致...

    java 字符集的解码方法

    `BufferedReader`则可以提供更高效的读取操作,它可以从`InputStreamReader`中读取字符: ```java BufferedReader bufferedReader = new BufferedReader(reader); String line; while ((line = bufferedReader....

    Java中的字符集编码入门(五)Java代码中的字符编码转换Part1.pdf

    Java通过统一采用UTF-16编码格式在JVM内部处理字符,简化了字符操作的复杂性。UTF-16是一种变长的Unicode编码,它可以表示Unicode字符集中所有的字符,每个字符通常由1至4个字节组成,但在Java中,每个`char`类型...

    Java io输入输出流及字符集

    本文将深入探讨Java中的输入输出流以及字符集的相关知识点。 一、Java IO流概述 Java的IO流模型是基于管道的概念,数据在不同设备之间流动就像水流在管道中传输一样。流可以分为四类:字节流(Byte Stream)和字符...

    java 字符串转16进制Ascii

    总结一下,Java中字符串转16进制ASCII涉及的关键点有: 1. `char`类型的字符与ASCII码的转换。 2. 使用`Integer.toHexString()`将ASCII码转换为16进制字符串。 3. 处理Unicode字符时,需要考虑字符集和编码方式。 ...

    JAVA 字符串 操作

    在Java编程语言中,字符串(String)是一个非常基础且重要的数据类型。它被广泛用于处理文本信息,例如用户输入、文件内容、网络数据等。本文将深入探讨Java中的字符串操作,包括创建、比较、拼接、查找与替换、分割...

    JAVA字符集

    Java字符集是一个涵盖编码基础知识、Java编程环境与字符编码关系以及不同编码标准如何在Java中应用的主题。在本文中,我们将深入探讨这些方面,以便更好地理解Java如何处理各种字符编码。 首先,我们要明白编码的...

    java文件字符编码检测和转换

    字符编码检测和转换 附件中:FileEncodeDetector.java 此文件可以检测指定文件的编码格式 public static String getFileEncode(File file) {...} 附件中:FileCharsetConverter.java 此文件可以实现两个编码的相互...

    json_lib使用实例:java对象与json字符串的互转

    1. **JSON字符串转Java对象** 使用`json-lib`,我们可以将JSON字符串解析为JavaBean、HashMap、ArrayList等。例如,假设我们有一个JSON字符串`{"name":"John", "age":30}`,可以这样转换: ```java String json...

    jdbc连接oracle字符集不同出现乱码

    这是因为 Java 在处理字符串时默认按照特定的字符集进行解码,而在本例中,Java 应用期望使用 `ZHS16GBK` 解码,但实际上却按照 `US7ASCII` 编码的数据进行了解码,从而导致乱码现象的发生。 #### 解决方案 为了...

    JAVA日期与字符串的转换

    ### JAVA日期与字符串的转换 在Java编程中,经常需要将日期对象转化为字符串形式以便于存储或显示,或者反过来将字符串转化成日期对象来进行日期计算等操作。本文将详细介绍如何在Java中实现这两种转换。 #### 一...

Global site tag (gtag.js) - Google Analytics