java中的编码

关于字符编码的方式很多,比如常用的有unicode系列的utf8,utf16,utf32,还有中国的gb2312,GBK,GBK18030等等,这些网上的资料很多,但都不是本集要讲的内容,本集要讲的内容只有三个

  1. java的文件编码
  2. java的string的外在编码
  3. java的string的内在编码,jvm用的编码

本文所有的讨论,直接基于jdk17,不见兼顾jdk8,所以,不会讨论历史的情况

1. java的文件编码

很多时候,大家都会说,防止文件乱码,然后统一使用utf-8,这样做没问题,但是为什么呢,这样做是对的,但是这样做的原因一定是对的嘛?

  1. java对于文件的编码方式是可以支持别的,不一定是utf-8,这个javac -help可以看到
  2. 用utf-8的确是一个很好的选择,即使你们公司都在国内,用gb2312编码也可以,但是如果,你如果使用了自动的ci,在服务器上编码代码,如果服务器是linux的,那么它的
    默认编码就可以是utf-8,那你代码编译的时候,就需要手动指定编码方式,而且如果你的项目是开源的,放到github上,那么世界各地都可能有committer,如果各个个国家的人,都用自己
    本国的编码,那项目就没法玩了,这个时候utf-8就是最佳的选择,所以,无论如何,文件编码使用utf-8都是很好的选择,至少现在就是
1
2
3
# https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html 
-encoding encoding
Specifies character encoding used by source files, such as EUC-JP and UTF-8. If the -encoding option is not specified, then the platform default converter is used.

java的string的外在编码

java的string的外在编码,指的是String.getBytes()方法使用的编码,这个可以看jdk的源码得到答案,一般oracle jdk会有一些私有代码,代码不是全部公开的,如果有些
时候看不到部分代码,可以使用开发的jdk,比如azul,这个时候,几乎就可以看到所有的代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public byte[] getBytes() {
//对外输出的代码,使用默认的编码,那什么是默认的编码呢
return encode(Charset.defaultCharset(), coder(), value);
}

public static Charset defaultCharset() {
if (defaultCharset == null) {
synchronized (Charset.class) {
//file.encoding 是默认的编码
String csn = GetPropertyAction
.privilegedGetProperty("file.encoding");
Charset cs = lookup(csn);
if (cs != null)
defaultCharset = cs;
else
//如果没有制定,就用默认的UTF-8
defaultCharset = sun.nio.cs.UTF_8.INSTANCE;
}
}
return defaultCharset;
}

java的string的内在编码,jvm用的编码

先下结论,java内在的编码,是utf-16,但是也不是,是UTF-16LE,因为java string的外在编码你只要看到的时候,其实就已经发生转换了,所以,你不能直接窥探到,这里我们
需要使用反射来窥探到,需要增加参数

1
2
3
4
--add-opens
java.base/java.lang=ALL-UNNAMED
--add-opens
java.base/sun.net.util=ALL-UNNAMED

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package pers.apricot.character;

import org.junit.jupiter.api.Test;

import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;

public class StringUnicodeTest {
@Test
void test1() throws NoSuchFieldException, IllegalAccessException {
HexFormat of = HexFormat.of();
String 我爱你 = new String("我爱你");
byte[] bytes = 我爱你.getBytes();
System.out.println(bytes.length);
System.out.println(of.formatHex(bytes));
Class<String> stringClass = String.class;
Field value = stringClass.getDeclaredField("value");
value.setAccessible(true);
byte[] originByteArr = (byte[]) value.get(我爱你);
System.out.println(originByteArr.length);
System.out.println(of.formatHex(originByteArr));
byte[] bytes1 = 我爱你.getBytes(StandardCharsets.UTF_16);
System.out.println(bytes1.length);
System.out.println(of.formatHex(bytes1));
byte[] bytes2 = 我爱你.getBytes(StandardCharsets.UTF_16BE);
System.out.println(bytes2.length);
System.out.println(of.formatHex(bytes2));
byte[] bytes3 = 我爱你.getBytes(StandardCharsets.UTF_16LE);
System.out.println(bytes3.length);
System.out.println(of.formatHex(bytes3));
}
}

输出如下

1
2
3
4
5
6
7
8
9
10
9
e68891e788b1e4bda0
6
11623172604f
8
feff621172314f60
6
621172314f60
6
11623172604f

可以看到直接用反射得到的字节,和UTF_16LE得到的字节是一样的,所以,java的确使用的是UTF_16LE

延伸

java为什么是UTF_16LE

这里应该和cpu的架构有关,比如x86的都是小端,little-endian,因为java是跨平台的吗,所以要入乡随俗,然后比如我的cpu是intel 9400,所以,自然就是小端的,当然这
个只是查的有关资料,并不是真的这样确定

为什么utf16多了两个字节

这里可以看到utf16,多了BOM,而且默认是使用大头

结论

  1. java的文件编码可以支持多种,但是最佳实践是采用utf-8
  2. java的string,默认编码也是utf-8,可以指定默认的,也可以转成其它的
  3. jvm的string编码,是utf16le或者utf16be

java编码终极指南2里面会详细介绍sun.jnu.encodingfile.encoding