Java使用哪种编码,UTF-8还是UTF-16?

17

我已经阅读了以下帖子:

  1. Java中字符串的内部表示是什么?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类的getBytes()方法,然后进入了StringCoding类的static byte[] encode(char[] ca, int off, int len)方法。在编码方法的第一行 (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根据底层操作系统决定使用哪种编码方式,还是仅使用UTF-16?请帮助我理解这个概念。


11
不要把getBytes()方法的默认编码与Java的内部编码混淆。 - Alohci
1
在Java中,无法访问String的内部表示方式。因此,您不必担心... - Robert
1
如果您想获取UTF-16字节表示,则使用character.getBytes(StandardCharsets.UTF_16) - Andy Turner
谢谢Andy!使用你的代码行,它会返回4个字节。 - Nitin Bhardwaj
嗨Alohci,"Java的内部编码"是什么意思?你能详细解释一下吗? - Nitin Bhardwaj
2个回答

53

字符是人类文化的一部分,是一个图形实体。当计算机需要处理文本时,它使用字节中的这些字符的一种表示形式。所使用的确切表示形式称为编码。

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

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

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

但是方法 getBytes() 不会返回这种内部表示。其文档说:

  

public byte[] getBytes()

     

使用平台的默认字符集将此 String 编码为字节序列,   将结果存储在新的字节数组中。

“平台的默认字符集”是您的区域设置变量所指定的字符集。也就是说,UTF-8。因此,它采用 UTF-16 内部表示,并将其转换为另一种表示形式 - UTF-8。

请注意

new String(bytes, StandardCharsets.UTF_16);

正如您所认为的,它并没有 "明确地将其转换为 UTF-16"。这个字符串构造函数接受一个字节序列,这个序列应该是在您在第二个参数中指定的编码中,然后将其转换为该编码中字节所表示字符的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) 不再返回内部表示。


然而,如果我没有弄错的话,Java在Char和String编码中使用UTF-16并不是一个内部实现细节,它可以对开发人员保持透明,因为一些Unicode代码点将占用字符串中的两个位置(如RealSkeptic所说),而Java 9(紧凑字符串)从这个角度来看并没有改变任何东西。 - Mabsten
1
@Mabsten,紧凑的8位表示法无论如何都不能表示那些字符。但它仍然是一种内部表示法。如果您使用charAt,它将返回一个16位UTF-16字符,而不管内部表示法如何。 - RealSkeptic
Java 这里难道没有命名错误吗?它使用 StandardCharsets 类型来指定字节的编码。字符集在这里是 Unicode,但我们真正设置的是编码。 - Burak.

2
如上所述,Java使用UTF-16作为字符数据的编码方式。此外,可表示的字符集仅限于整个Unicode字符集的一个适当子集。(我相信Java将其字符集限制为Unicode BMP,所有这些字符都适合UTF-16的两个字节。)因此,应用的编码确实是UTF-16,但应用于其上的字符集是整个Unicode字符集的一个适当子集,这保证了Java在其内部字符串编码中始终使用每个标记两个字节。

2
这对于当前的Java版本是不正确的。在String对象中,Java使用代理对(这是UTF-16定义的一部分)表示超出BMP范围的字符。因此,char类型确实不能表示BMP范围外的字符,但Java String绝对可以。 - RealSkeptic
1
有趣。那么对于这样的字符串,调用charAt()或getChars()会发生什么? - Erwin Smout
4
当你编写需要注意这些字符的程序时,你需要使用适当的方法,例如使用 codePointAt(int) 代替 charAt(int)codePointCount(int,int) 代替 length()等。 - RealSkeptic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接