编码和解码


编码和解码

​ 平时开发的时候经常会遇到字符串编码和解码的问题,今天就初步探究下乱码问题,寻求解决乱码的方法。

概念

首先来分析下Java中的编码问题,文本在存储设备中是以字节的方式存储的,以特定的编码方案讲编码映射到字节数组。从存储设备中读取文本文件和写入文本就涉及到编码和解码。在JVM中,文本是以Unicode编码的形式存在的。下面先讲四个概念:

  • 字符集合(Charater set): 是一组形状的集合,比如所有汉字的集合,它体现了字符的’’形状’’。
  • 编码字符集(Coded character set): 是一组字符对应的编码(即数字),字符集中的每一个字符对应一个数字。在Java中每一个字符可以认为是一个16位的数字。
  • 字符编码方案(Character-encoding schema): 讲字符编码映射到一个字节数组的方案,因为在磁盘中,所有信息都是以字节的方式存储的。因此Java中的16位编码必须转换成一个字节数组才能够存储。常见的编码方案有UTF-8、GBK等。
  • 字符集(Charset):编码字符集和字符编码方案合起来称为字符集。

转换

编码

从JVM中的Unicode编码到字节数组,这个转换过程被称之为编码。转换的目的是为了存储或发送信息。同一个Unicode编码采用不同的字符集进行编码会得到不同的字节数组。编码的例子如下:

1
2
3
4
5
6
7
8
//String的getBytes方法
String s = "编码";
byte[] b1 = s.getBytes();//getBytes会调用StringCoding类的encode方法,先用Charset的默认编码方案,如果不支持,再用ISO-8859-1,如果还不支持就exit(1).
//Charset的encode方法
Charset charset = Charset.forName("UTF-8");
ByteBuffer byteBuffer = charset.encode(s);
byte[] b2 = new byte[byteBuffer.remaining()];
byteBuffer.get(b2);

解码

从字节数组到Unicode编码,这个转换过程被称之为解码。解码一般发生在需要从磁盘或者网络上得到的字节数组转换为字符串的场景。解码的时候一定要指定字符集,否则会使用默认的字符集进行解码。如果字符集错误,则会出现乱码。解码的例子如下:

1
2
3
4
5
6
7
//使用String构造方法
String s1 = new String(b1, "UTF-8");
//使用Charset的decode方法
Charset cset = Charset.forName("UTF-8");
ByteBuffer buffer = ByteBuffer.wrap(b2);
CharBuffer charBuffer = cset.decode(buffer);
String s2 = charBuffer.toString();

默认字符集

Java的默认字符集有两种设置方法:

  • 在执行Java程序的时候使用-Dfile.encoding参数指定
  • 使用Properties类指定字符集。

两种方法同时使用的话,则在程序开始的时候使用参数指定的字符集,在执行Properties方法之后使用Properties指定的字符集。如果没有显式指定默认字符集,则使用操作系统默认的字符集。如果以上字符集无效,则会采用JDK中默认的字符集ISO-8859-1

乱码与解决方案

乱码的原因

从上文可知,乱码的产生原因在于解码。采用了错误的字符集解码字节数组就会产生乱码。下面是个小例子:

1
2
3
4
5
6
7
8
String before = "中国";
byte[] bytes = before.getBytes("UTF-8");
String after = new String(bytes, "GBK");
System.out.println(before);
System.out.println(after);
//输出结果:
//中国
//涓浗

解决方案

​ 在一般情况下我们可以得知字节数组的编码方案,在解码的时候指定正确的字符集就可以避免乱码。但有些时候我们并不能得知或者很难得知字节数组所采用的编码方案,那么按照常用的字符集进行解码的时候就很容易造成乱码。

在我们知道字节数组使用了哪几种编码方案之一的时候,我们可以判断得到的字符串在某个字符集下时候有效。日常开发中常见的字符编码方案有UTF-8、GBK、ISO-8859-1三种,其中ISO-8859-1是西欧标准,用这种编码方案编码中文等非拉丁语言的时候会得到值为63的一个字节。GBK编码中文采用的是两个字节,UTF-8采用的是变长编码方式,这两种可以采用以下方法判断具体是哪种编码方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static boolean isStringValidInCharset(String s, String charset) {
try {
// 获取字符串s在字符集charset下的编码
byte[] bytes = s.getBytes(charset);
// 把获得的编码按该字符集解码成新字符串
String ss = new String(bytes, charset);
// 解码出来的新字符串应该与原来相等
// 如果s不是该字符集下有效的字符串,解码出来的会是一堆问号
return s.equals(ss);
} catch (UnsupportedEncodingException e) {
return false;
}
}
//测试用例:
//System.out.println(isStringValidInCharset("中文abc", "GBK"));
//System.out.println(isStringValidInCharset("图书", "GBK"));
//输出:
//rue
//false

上述方法只能简单的判断几种编码方案,下面介绍一种基于统计学的推断编码方案的工具cpdetector。cpdetector按照谁先返回非空探测结果,就以该结果为准的原则返回探测到的字符集编码。从理论上来说,只要字符串越长,返回结果正确的概率就越大。

1
2
3
4
5
6
7
8
File file = new File(filePath);
CodepageDetectorProxy detector = CodepageDetectorProxy.getInstance();
detector.add(new ParsingDetector(false));
detector.add(JChardetFacade.getInstance());
detector.add(ASCIIDetector.getInstance());
detector.add(UnicodeDetector.getInstance());
java.nio.charset.Charset charset = null;
charset = detector.detectCodepage(file.toURI().toURL());

文章作者: Amos Liu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Amos Liu !
 上一篇
算法练习3 算法练习3
算法练习3Longest Palindromic SubstringGiven a string s, find the longest palindromic substring in s. You may assume that the
2017-05-03
下一篇 
Java字符串 Java字符串
Java字符串​ 在平时开发的过程中,字符串操作应该是最常见的行为。而在Java中,String类大概是我们使用的最频繁的一个类了。今天我们就来初步研究下String的实现。说起看源码,就本能的感到一种对高阶程序员的一种畏惧,但当你打
2017-04-15
  目录