【问题标题】:From compilation to runtime, how does Java String encoding really work从编译到运行时,Java String 编码究竟是如何工作的
【发布时间】:2011-01-11 00:15:08
【问题描述】:

最近发现自己对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 */
    }
}

由于控制字符为interpreted differently between windows-1252 and ISO-8859-1,所以我选择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

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

这让我想到了几个问题:

  1. 正在编译的 Java 文件的代码页/字符集是否应该与正在编译它的系统的默认字符集相同?两者总是同义词吗?
  2. 编译后的表示似乎不依赖于编译时字符集,真的是这样吗?
  3. 这是否意味着如果 Java 文件中的字符串不使用当前字符集/语言环境的标准字符,则它们在运行时可能会被不同地解释?
  4. 关于 Java 中的字符串和字符编码,我还应该真正了解哪些内容?

【问题讨论】:

  • 不清楚你所说的“用不同的文件编码编译它”是什么意思。你的意思是你用不同的编码保存文件,然后使用 -encoding 开关编译每个文件到 javac?如果是这样,在将源文件保存为这些编码后,您如何知道源文件中出现了哪些随机垃圾?您不能将文字控制字符放入源代码中并期望它能够在序列化为编码字符后继续存在。
  • 一个文件只不过是一个字节流。这些字节的解释不同,具体取决于它们假定的字符编码。因此,我指的是包含chars 的字符串,通过假设文件,在运行时或编译时可能会有不同的解释以不同的字符集编码。
  • 为了明确编译步骤,我使用 sun 的 encoding 属性在编译时设置字符集:javac -encoding windows-1252 Main.java,并适当地设置了编码。

标签: java string character-encoding


【解决方案1】:

如果您使用不同的编码进行编译,这些编码只会影响您的源文件。如果您的源代码中没有任何特殊字符,则生成的字节码不会有任何差异。

对于运行时,使用操作系统的默认字符集。这与您用于编译的字符集无关。

【讨论】:

    【解决方案2】:

    Erm 基于thisthis 的 ACK 控制字符在两种编码中完全相同。您指出的链接的区别在于 DOS/Windows 实际上如何为 Windows-1252 中的大多数控制字符(如 Heart/Club/Spade/Diamond 字符和类似符号)提供符号,而 ISO-8859 没有。

    【讨论】:

    • 你是对的,这两种编码中的 ack char 都是 0x06 。也许我失败了,但我试图想出一个场景,根据当前的字符集对它进行不同的解释。 @McDowell 的博客文章在展示我尝试做的事情方面做得更好。
    【解决方案3】:
    1. 源文件可以采用任何编码
    2. 您需要告诉编译器源文件的编码(例如javac -encoding...);否则,假定平台编码
    3. 在类文件二进制文件中,字符串文字存储为(修改后的)UTF-8,但除非您使用字节码,否则这无关紧要(请参阅JVM spec
    4. Java 中的字符串始终为 UTF-16(请参阅 Java language spec
    5. System.outPrintStream 会将您的字符串从 UTF-16 转换为系统编码中的字节,然后再将它们写入 stdout

    注意事项:

    【讨论】:

      【解决方案4】:

      Java 中字符串编码的“须知”总结:

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

      实际上,您应该在代码中明确指定编码,至少大部分时间是这样。请勿拨打String.getBytes(),请拨打String.getBytes("UTF-8")。当应用于与用户交换的某些数据(例如配置文件或要立即显示的消息)时,可以使用默认的、与区域设置相关的编码;但在其他地方,尽可能避免依赖于语言环境的方法。

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-12-05
        • 1970-01-01
        相关资源
        最近更新 更多