从编译到运行时,Java String编码如何真正起作用

我最近意识到我并不完全理解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之间有不同的解释 ,因此我选择了ack char进行测试。

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

然后我运行程序:

 $ 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

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

这引出了几个问题:

  1. 编译的Java文件的代码页/字符集是否应该与编译它的系统的默认字符集相同? 两者总是同义词吗?
  2. 编译后的表示似乎不依赖于编译时的字符集,这是否确实如此?
  3. 这是否意味着如果Java文件中的字符串不使用当前字符集/语言环境的标准字符,那么它们在运行时可能会有不同的解释?
  4. 关于Java中的字符串和字符编码我还应该知道什么呢?

  1. 源文件可以是任何编码
  2. 你需要告诉编译器源文件的编码(例如javac -encoding... ); 否则,假设平台编码
  3. 在类文件二进制文件中,字符串文字存储为(已修改)UTF-8,但除非使用字节码,否则无关紧要(请参阅JVM规范 )
  4. Java中的字符串是UTF-16,总是(参见Java语言规范 )
  5. 在将字符串写入stdout之前, System.out PrintStream会将字符串从UTF-16转换为系统编码中的字节

笔记:

  • 我用Java编码写的博客文章
  • 不要使用-Dfile.encoding

关于Java中字符串编码的“知道什么”的摘要:

  • 内存中的String实例是一个16位“代码单元”序列,Java将其作为char值处理。 从概念上讲,这些代码单元编码一系列“代码点”,其中代码点是“根据Unicode标准归因于给定字符的数字”。 代码点的范围从0到超过一百万,尽管到目前为止只定义了大约100,000左右。 从0到65535的代码点被编码为单个代码单元,而其他代码点使用两个代码单元。 此过程称为UTF-16(又名UCS-2)。 存在一些细微之处(一些代码点无效,例如65535,并且在第一个65536中存在2048个代码点的范围,其精确地保留用于其他代码点的编码)。
  • 代码页等不会影响Java如何将字符串存储在RAM中。 这就是“Unicode”以“Uni”开头的原因。 只要您不使用字符串执行I / O,您就处于Unicode世界中,每个人都使用相同的字符映射到代码点。
  • Charsets在将字符串编码为字节或从字节解码字符串时开始行动。 除非明确指定,否则Java将使用默认的字符集,该字符集取决于用户“locale”,这是一个模糊聚合概念,说明了日本的计算机是什么日语。 当您使用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中,您可能希望对字符串执行的某些操作依赖于区域设置。 这不是编码问题; 但是语言环境也定义了一种“语言”,而大写和小写的概念恰好取决于所使用的语言。 Usual Suspect正在调用"unicode".toUpperCase() :这会产生"UNICODE"除非当前语言环境是土耳其语,在这种情况下你得到"UNİCODE" (“ I ”有一个点)。 这里的基本假设是,如果当前语言环境是土耳其语,那么应用程序管理的数据可能是土耳其语文本; 就个人而言,我发现这个假设至多值得怀疑。 但事实确实如此。

实际上,您应该在代码中明确指定编码,至少在大多数情况下。 不要调用String.getBytes() ,调用String.getBytes("UTF-8") 。 当应用于与用户交换的某些数据时,使用默认的,与语言环境相关的编码很好,例如配置文件或要立即显示的消息; 但在其他地方,尽可能避免使用与语言环境相关的方法

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

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

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

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

Erm基于此 , 这两个编码中的ACK控制字符完全相同。 您指出的链接的差异在于谈论DOS / Windows实际上如何为Windows-1252中的大多数控制字符(如Heart / Club / Spade / Diamond字符和simileys)提供符号,而ISO-8859则没有。