从编译到运行时,Java字符串编码如何真正工作

2022-09-02 08:57:57

我最近意识到我并不完全了解Java的字符串编码过程。

请考虑以下代码:

public class Main
{
    public static void main(String[] args)
    {
        System.out.println(java.nio.charset.Charset.defaultCharset().name());
        System.out.println("ack char: ^"); /* where ^ = 0x06, the ack char */
    }
}

由于 windows-1252 和 ISO-8859-1 之间的控制字符解释不同,因此我选择了 char 进行测试。ack

我现在使用不同的文件编码,UTF-8,windows-1252ISO-8859-1编译它。两者编译为完全相同的东西,由 验证的每字节字节。md5sum

然后我运行该程序:

$ java Main | hexdump -C
00000000  55 54 46 2d 38 0a 61 63  6b 20 63 68 61 72 3a 20  |UTF-8.ack char: |
00000010  06 0a                                             |..|
00000012

$ java -Dfile.encoding=iso-8859-1 Main | hexdump -C
00000000  49 53 4f 2d 38 38 35 39  2d 31 0a 61 63 6b 20 63  |ISO-8859-1.ack c|
00000010  68 61 72 3a 20 06 0a                              |har: ..|
00000017

$ java -Dfile.encoding=windows-1252 Main | hexdump -C
00000000  77 69 6e 64 6f 77 73 2d  31 32 35 32 0a 61 63 6b  |windows-1252.ack|
00000010  20 63 68 61 72 3a 20 06  0a                       | char: ..|
00000019

无论使用哪种编码,它都会正确输出 。0x06

好的,它仍然输出相同的,这将被windows-1252代码页解释为可打印的[ACK]char。0x06

这让我想到了几个问题:

  1. 正在编译的 Java 文件的代码页/字符集是否应与编译它的系统的缺省字符集相同?这两者总是同义词吗?
  2. 编译的表示形式似乎不依赖于编译时字符集,事实果真如此吗?
  3. 这是否意味着如果 Java 文件中的字符串不对当前字符集/区域设置使用标准字符,则在运行时可能会有不同的解释?
  4. 关于Java中的字符串和字符编码,我还需要了解哪些内容?

答案 1
  1. 源文件可以采用任何编码
  2. 您需要告诉编译器源文件的编码(例如);否则,假定采用平台编码javac -encoding...
  3. 在类文件二进制文件中,字符串文本存储为(修改后的)UTF-8,但除非您使用字节码,否则这无关紧要(请参阅 JVM 规范)
  4. Java 中的字符串始终是 UTF-16(请参阅 Java 语言规范)
  5. 打印流会将您的字符串从 UTF-16 转换为系统编码中的字节,然后再将其写入 stdoutSystem.out

笔记:


答案 2

关于Java中字符串编码的“须知事项”摘要:

  • 内存中的实例是一系列 16 位“代码单元”,Java 将其作为值进行处理。从概念上讲,这些代码单元编码一系列“码位”,其中码位是“根据Unicode标准归因于给定字符的数字”。码位的范围从 0 到略高于 100 万,尽管到目前为止只定义了 10 万个左右。从 0 到 65535 的代码点编码为单个代码单元,而其他代码点使用两个代码单元。此过程称为 UTF-16(也称为 UCS-2)。有一些微妙之处(一些代码点无效,例如65535,并且在第一个65536中有一个2048个代码点的范围,专门用于其他代码点的编码)。Stringchar
  • 代码页等不会影响Java在RAM中存储字符串的方式。这就是为什么“Unicode”以“Uni”开头。只要你不用字符串执行 I/O,你就处于 Unicode 世界中,每个人都使用相同的字符到码位的映射。
  • 将字符串编码为字节或从字节解码字符串时,字符集将起作用。除非明确指定,否则Java将使用默认字符集,该字符集取决于用户“locale”,这是一个模糊的聚合概念,使日本的计算机说日语。当您使用 打印出字符串时,JVM 会将字符串转换为适合这些字符的任何内容,这通常意味着使用字符集将它们转换为字节,该字符集取决于当前语言环境(或 JVM 对当前语言环境的猜测)。System.out.println()
  • 一个Java应用程序是Java编译器。Java编译器需要解释源文件的内容,在系统级别,源文件只是一堆字节。然后,Java编译器会为此选择一个默认字符集,并且它根据当前区域设置执行此操作,就像Java所做的那样,因为Java编译器本身是用Java编写的。Java 编译器 () 接受一个命令行标志 (),该标志可用于覆盖该缺省选项。javac-encoding
  • Java 编译器生成独立于语言环境的类文件。字符串文本最终出现在那些具有(某种程度上)UTF-8 编码的类文件中,而不管 Java 编译器用于解释源文件的字符集如何。运行 Java 编译器的系统上的区域设置会影响源代码的解释方式,但是一旦 Java 编译器理解您的字符串包含编号 6 的代码点,那么此代码点将进入类文件,而不是其他代码点。请注意,码位 0 到 127 在 UTF-8、CP-1252 和 ISO-8859-1 中具有相同的编码,因此您获得的编码也就不足为奇了。
  • 即便如此,实例也不依赖于任何类型的编码,只要它们保留在RAM中,您可能希望对字符串执行的某些操作都依赖于区域设置。这不是编码问题;但是区域设置也定义了一种“语言”,因此大写和小写的概念取决于使用的语言。通常的嫌疑人正在呼叫:除非当前区域设置是土耳其语,否则这会产生结果,在这种情况下,您将获得(“”有一个点)。这里的基本假设是,如果当前区域设置是土耳其语,则应用程序管理的数据可能是土耳其语文本;就个人而言,我发现这种假设充其量是有问题的。但事实也的确如此。String"unicode".toUpperCase()"UNICODE""UNİCODE"I

实际上,至少在大多数情况下,您应该在代码中显式指定编码。不要打电话,打电话。当默认的,依赖于区域设置的编码应用于与用户交换的某些数据时,可以使用默认的,依赖于区域设置的编码,例如要立即显示的配置文件或消息;但在其他地方,尽可能避免使用依赖于区域设置的方法。String.getBytes()String.getBytes("UTF-8")

在Java的其他依赖于语言环境的部分之外,还有日历。有整个时区业务,这取决于“时区”,这应该与计算机的地理位置有关(这不是严格意义上的“区域设置”的一部分...)。此外,无数Java应用程序在曼谷运行时神秘地失败了,因为在泰国语言环境中,Java默认为佛教日历,根据该日历,当前年份为2553。

根据经验,假设世界是广阔的(它是!)并保持通用(不要做任何依赖于字符集的事情,直到最后一刻,当I / O必须实际执行时)。