Java 使用的是 UTF-8 还是 UTF-16 编码?

Which encoding does Java uses UTF-8 or UTF-16?

我已经阅读了以下帖子:

  1. What is the Java's internal represention for String? Modified UTF-8? UTF-16?
  2. https://docs.oracle.com/javase/8/docs/api/java/lang/String.html

现在考虑下面给出的代码:

public static void main(String[] args) {
    printCharacterDetails("最");
}

public static void printCharacterDetails(String character){
    System.out.println("Unicode Value for "+character+"="+Integer.toHexString(character.codePointAt(0)));
    byte[] bytes = character.getBytes();
    System.out.println("The UTF-8 Character="+character+"  | Default: Number of Bytes="+bytes.length);
    String stringUTF16 = new String(bytes, StandardCharsets.UTF_16);
    System.out.println("The corresponding UTF-16 Character="+stringUTF16+"  | UTF-16: Number of Bytes="+stringUTF16.getBytes().length);
    System.out.println("----------------------------------------------------------------------------------------");
}

当我尝试调试上面代码中的 character.getBytes() 行时,调试器将我带到了 String class 的 getBytes() 方法,然后进入了 static byte[] encode(char[] ca, int off, int len) StringCoding 方法 class。 encode 方法的第一行 (String csn = Charset.defaultCharset().name();) 在调试期间返回 "UTF-8" 作为默认编码。我预计它会是 "UTF-16"。

程序的输出是:

Unicode 值最=6700 UTF-8 字符=最 |默认值:字节数=3

对应的UTF-16字符=� | UTF-16:字节数=6

当我在程序中将它显式转换为 UTF-16 时,它需要 6 个字节来表示字符。它不应该为 UTF-16 使用 2 或 4 个字节吗?为什么使用了 6 个字节?

我的理解哪里出错了? 我使用 Ubuntu 14.04,locale 命令显示如下:

LANG=en_US.UTF-8

这是否意味着 JVM 根据底层 OS 决定使用哪种编码,还是仅使用 UTF-16? 请帮助我理解这个概念。

字符是图形实体,是人类文化的一部分。当计算机需要处理文本时,它会使用这些字符的表示(以字节为单位)。使用的确切表示称为 encoding.

有许多编码可以表示相同的字符 - 通过 Unicode 字符集,或通过其他字符集,如各种 ISO-8859 编码或 JIS X 0208。

在内部,Java 使用 UTF-16。这意味着每个字符都可以由一个或两个两个字节的序列表示。您使用的字符最具有代码点 U+6700,在 UTF-16 中表示为字节 0x67 和字节 0x00。

那是内部编码。你看不到它,除非你转储内存并查看转储图像中的字节。

但是方法 getBytes() 没有 return 这种内部表示。它的文档说:

public byte[] getBytes()

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

"platform's default charset" 是您的语言环境变量所说的。即UTF-8。所以它采用 UTF-16 内部表示,并将其转换为不同的表示 - UTF-8。

注意

new String(bytes, StandardCharsets.UTF_16);

不是"convert it to UTF-16 explicitly",正如您假设的那样。这个字符串构造函数接受一个字节序列,它应该是你在第二个参数中给出的编码,并将其转换为这些字节在该编码中表示的任何字符的 UTF-16 表示。

但是你已经给了它一个以 UTF-8 编码的字节序列,并告诉它把它解释为 UTF-16。这是错误的,您没有得到您期望的字符或字节。

您无法告诉 Java 如何在内部存储字符串。它始终将它们存储为 UTF-16。构造函数 String(byte[],Charset) 告诉 Java 从应该在给定字符集中的字节数组创建 UTF-16 字符串。方法 getBytes(Charset) 告诉 Java 给你一个字节序列,代表给定编码(字符集)中的字符串。不带参数的方法 getBytes() 的作用相同 - 但使用平台的默认字符集进行转换。

所以你误解了 getBytes() 给你的东西。它不是内部表示。你不能直接得到它。只有 getBytes(StandardCharsets.UTF_16) 会给你那个,而且只是因为你知道 UTF-16 是 Java 的内部表示。如果 Java 的未来版本决定以不同的编码表示字符,那么 getBytes(StandardCharsets.UTF_16) 将不会向您显示内部表示。

编辑: 事实上,Java 9 引入了字符串内部表示的这种变化,默认情况下,所有字符都属于 ISO 的字符串-8859-1 范围在内部以 ISO-8859-1 表示,而至少有一个字符超出该范围的字符串在内部以 UTF-16 表示,如前所述。所以确实,getBytes(StandardCharsets.UTF_16) 不再是 return 的内部表示。

如上所述,java 使用 UTF-16 作为字符数据的编码。

可以补充的是,可表示的字符集仅限于整个 Unicode 字符集的适当子集。 (我相信 java 将其字符集限制为 Unicode BMP,所有这些都适合两个字节的 UTF-16。)

所以应用的编码确实是 UTF-16,但是应用它的字符集是整个 Unicode 字符集的一个适当的子集,这保证了 Java 每个标记总是使用两个字节在其内部字符串编码中。