2010-01-29 100 views
16

我最近意识到我没有完全理解Java的字符串编码过程。从编译到运行时,Java String编码如何真正起作用

考虑下面的代码:

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中的字符串和字符编码?
+0

目前还不清楚“用不同的文件编码进行编译”是什么意思。你的意思是你用不同的编码保存文件,然后使用-encoding开关编译每个文件到javac?如果是这样,你怎么知道在将这些随机垃圾保存在这些编码中之后,什么随机垃圾正在清空?你不能将一个文字控制字符放入你的源代码中,并期望它能够在序列化到编码字符之后继续存在。 – 2010-01-29 20:14:56

+0

文件不过是一个字节流。这些字节根据它们被假定为的字符编码而被不同地解释。因此,我指的是包含'char'的字符串,这些字符可以在运行时或在编译时以不同的方式解释,假设文件被编码在不同的字符集中。 – 2010-01-29 20:20:26

+0

为了明确编译步骤,我使用了sun的编码属性在编译时设置字符集:'javac -encoding windows-1252 Main.java',编码设置得当。 – 2010-01-29 21:07:10

回答

19
  1. 源文件可以在任何编码
  2. 你需要告诉编译器源文件的编码(例如javac -encoding...);否则,假设平台编码
  3. 在类文件的二进制文件,字符串存储为(修改)UTF-8,但除非你用的字节码的工作,这并不重要(见JVM spec
  4. 字符串在Java中是UTF -16,总是(见Java language spec
  5. System.outPrintStream将它们写之前将您的字符串从UTF-16系统中的编码字节标准输出

注:

3

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

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

1

基于thisthis的Erm在两种编码中ACK控制字符完全相同。您指出的区别在于讨论了DOS/Windows实际上是否具有Windows-1252中大多数控制字符的符号(如Heart/Club/Spade/Diamond字符和simileys),而ISO-8859没有。

+0

你是对的,在这两种编码中,ack char都是0x06。也许我失败了,但我试图想出一个基于当前字符集将会被不同解释的场景。 @ McDowell的博客文章在展示我试图做的事情方面做得更好。 – 2010-01-29 21:18:34

13

的 “知道” 关于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将使用取决于用户“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中,您可能希望在字符串上执行的某些操作是与语言环境相关的。这不是编码问题;但区域设置也定义了一种“语言”,所以大写和小写的概念取决于所使用的语言。常用疑似呼叫"unicode".toUpperCase():除非当前语言环境为土耳其语,否则将产生"UNICODE",在这种情况下,您将得到"UNİCODE"(“I”有一个点)。这里的基本假设是,如果当前的语言环境是土耳其语,那么应用程序管理的数据可能是土耳其文;就我个人而言,我认为这个假设最好是可疑的。但事实确实如此。

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

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

作为一个经验法则,认为世界是巨大的(它是!)并保持通用性(不要做任何依赖于字符集的任何事情,直到最后一刻,当I/O必须实际执行时)。