编码

从字节到字符

编码原因


计算机不能直接表示各种文字,只能二进制,文字只能映射到数值
任何文字底层全是byte,编码决定了二进制流怎么切割,怎么翻译成文字

各地编码


  • ASCII
    也称为ANSI, 7位编码,数字、字母、标点、控制符,美国编码标准
  • ISO-8859-1
    也称为Latin-1,单字节编码,兼容ASCII,加入了带有注音字母等扩展字符,适用于欧洲的编码标准
  • GB2312
    变长,小于127为ASCII码单字节,大于127的两个字节构成一个汉字,国标编码标准,还新编了2字节版本的ASCII码作为全角字符,共涵盖6000+汉字
  • GBK
    国标编码扩展,兼容GB2312,支持繁体,涵盖20000+汉字
  • GB18030
    GBK基础上新增了少数民族文字,最长可以4字节Unicode

国际统一编码


Unicode(Universal Multiple-Octet Coded Character Set,UCS), UCS-2两字节,UCS-4四字节,国际编码标准
Unicode和各个编码都不兼容,只是前面值可视为单字节的编码点和ISO-8859-1一致
Unicode转GB系列需要查表,而转UTF系列使用算法映射

Unicode制定了字符集,如果直接按照Unicode传输比较浪费,而UTF(UCS Transfer Format)定义了不同的表现形式

  • UTF-8
    变长,8位为码元,用1~4个码元编码,英文1字节,中文3字节
  • UTF-32
    定长,所有都用4字节表示
  • UTF-16
    变长,2或4字节大部分都是2字节,16位为码元,字符集被划分成基本平面和辅助平面,位于基本平面的1个码元,位于辅助平面的2个码元

UTF8确定长度规则

  • 字节最高位是0,编码只有一个字节
  • 字节开头是11,连续1的个数表示编码字节数
  • 字节开头是10,非首字节,需要到前面查找首字节

比较

在网络传输情况下,可能损坏单字节

  • Utf-8变长节省流量,单字节单位有规则确认位置,即使损坏也不会影响后文
  • Utf-16基本定长,更适合内存和硬盘中使用

BOM

BOM(Byte Order Mark),值FE FF表示大端序,值FF FE表示小端序
UTF-8单字节为单位,不存在字节序问题无需BOM。但是也允许携带作为标志EF BB BF
Windows平台有时编辑器也会加BOM,因此务必选择UTF-8无BOM模式。

Java中的编解码


  1. Java源代码的编码可以自由指定,IDE可以设置,最终表现为javac的encoding参数,如果不指定就会操作系统编码,即要告知编译器源文件是什么编码
  2. 编译时会进行编码转换,编译后的class文件采用Utf-8编码存储
  3. 为了支持国际化,Java字符串采用Unicode编码。JVM运行时内部采用Utf-16编码,因而Java中的char是16位双字节。虽然比较耗空间,但是相对定长更易于处理。
  4. System.out输出时由Utf-16转为系统编码
  5. 解析外部资源,使用file.encoding指定的编码,默认Utf-8

字符集类

java.nio.charset.Charset提供编码字符集相关功能,JVM解析外部资源所使用的编码由defaultCharset方法获得,与JVM启动时的操作系统默认编码有关,默认编码首先检查System.getProperty("file.encoding")是否指定,没设置就采用UTF-8作为默认编码
volatile + sync延迟加载模式

Charset.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static volatile Charset defaultCharset;

public static Charset defaultCharset() {
if (defaultCharset == null) {
synchronized (Charset.class) {
String csn = AccessController.doPrivileged(new GetPropertyAction("file.encoding"));
Charset cs = lookup(csn);
if (cs != null)
defaultCharset = cs;
else
defaultCharset = forName("UTF-8");
}
}
return defaultCharset;
}

Charset可以直接用于编解码,在ByteBuffer和CharBuffer之间转换

Charset.java
1
2
3
4
5
6
public final ByteBuffer encode(CharBuffer cb) {
//...
}
public final CharBuffer decode(ByteBuffer bb) {
//...
}

内置字符集名称

直接以字符串形式手写编码类型是不太好的形式,至少也应该是常量定义
Java 1.7之后存在java.nio.charset.StandardCharsets,无需使用第三方库,直接引用

StandardCharsets.java
1
2
3
4
5
6
7
8
9
10
11
public final class StandardCharsets {
private StandardCharsets() {
throw new AssertionError("No java.nio.charset.StandardCharsets instances for you!");
}
public static final Charset US_ASCII = Charset.forName("US-ASCII");
public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
public static final Charset UTF_8 = Charset.forName("UTF-8");
public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
public static final Charset UTF_16 = Charset.forName("UTF-16");
}

字符串的编解码


  • 文字转byte数组,编码过程
    String类提供的getBytes方法可以获得文字编码后的字节数组结果,可以指定编码或者使用默认编码
Sample
1
2
3
4
5
6
7
8
9
10
11
DatatypeConverter.printHexBinary("Java".getBytes("UTF-8")) //4A 61 76 61 每个英文只用1字节编码
DatatypeConverter.printHexBinary("编码".getBytes("UTF-8")) //E7BC96 E7A081 每个中文3字节,E开头1110

DatatypeConverter.printHexBinary("Java".getBytes("UTF-16")) //FEFF 004A 0061 0076 0061 BOM+每个英文2字节编码
DatatypeConverter.printHexBinary("编码".getBytes("UTF-16")) //FEFF 7F16 7801 BOM+每个中文2字节

DatatypeConverter.printHexBinary("Java".getBytes("ISO-8859-1")) //4A 61 76 61 单字节编码
DatatypeConverter.printHexBinary("编码".getBytes("ISO-8859-1")) //3F 3F 中文变成3F,也就是?,丢失信息,编码黑洞

DatatypeConverter.printHexBinary("Java".getBytes("GB2312")) //4A 61 76 61 每个英文只用1字节编码
DatatypeConverter.printHexBinary("编码".getBytes("GB2312")) //B1E0 C2EB 中文2字节

获取和指定Unicode

Sample
1
2
System.out.println(DatatypeConverter.printHexBinary("编码".getBytes("Unicode"))); //FEFF 7F16 7801
System.out.println("\u7F16\u7801"); //编码
  • byte数组转文字,解码过程
    编解码使用同一规则才能正确,否则会出现乱码
Sample
1
2
3
4
byte[] src = "编码".getBytes("Utf-8");
System.out.println(new String(src, "Utf-8")); //编码
System.out.println(new String(src, "GBK")); //缂栫爜
System.out.println(new String(src, "Iso-8859-1")); //编ç

对于ASCII,因为都兼容,因此不会出现问题

Sample
1
2
3
4
byte[] src = "Java".getBytes("Utf-8");
System.out.println(new String(src, "Utf-8")); //Java
System.out.println(new String(src, "GBK")); //Java
System.out.println(new String(src, "Iso-8859-1")); //Java

IO编解码


InputStream/OutputStream读字节流,Reader/Writer读字符流,InputStreamReader/OutputStreamWriter实现了字节到字符的转化

字节流处理二进制数据,是不考虑编解码的,所以FileInputStream并无任何处理编解码功能

InputStreamReader进行了字节字符转化,可以接受字符集参数,通过内部的StreamDecoder进行处理

InputStreamReader.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public InputStreamReader(InputStream in) {
super(in);
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
// The default encoding should always be available
throw new Error(e);
}
}
public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException {
super(in);
if (charsetName == null)
throw new NullPointerException("charsetName");
sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
}

FileReader是InputStreamReader的简单继承,构造时没有传入字符集参数,只能使用默认编码,因此需要制定编码时不能直接用FileReader

FileReader.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class FileReader extends InputStreamReader {
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}

public FileReader(File file) throws FileNotFoundException {
super(new FileInputStream(file));
}

public FileReader(FileDescriptor fd) {
super(new FileInputStream(fd));
}
}