为什么在允许某些Unicode字符的注释中执行Java代码?
下面的代码产生输出“Hello World!” (没有真的,试试看)。
public static void main(String... args) { // The comment below is not a typo. // \u000d System.out.println("Hello World!"); }
原因在于Java编译器将Unicode字符\u000d
parsing为新行,并转换为:
public static void main(String... args) { // The comment below is not a typo. // System.out.println("Hello World!"); }
从而导致评论被“执行”。
既然这可以用来“隐藏”恶意代码,或者一个邪恶的程序员能想象的, 为什么它可以在评论中被允许 ?
为什么Java规范允许这样做?
Unicode解码发生在任何其他词汇翻译之前。 这样做的关键好处就是它可以在ASCII和其他编码之间来回切换。 你甚至不需要找出评论开始和结束的地方!
如JLS第3.3节所述,这允许任何基于ASCII的工具来处理源文件:
Java编程语言指定了一种将用Unicode编写的程序转换为ASCII的标准方法,该程序将程序转换为可由基于ASCII的工具处理的格式。 […]
这为平台独立性(支持字符集的独立性)提供了基本的保证,这一直是Java平台的关键目标。
能够在文件中的任何位置编写任何Unicode字符是一个整洁的function,在以非拉丁语言编写代码时,在注释中尤其重要。 它可以以这种微妙的方式干扰语义的事实只是一个(不幸的)副作用。
在这个主题上有很多陷阱,Joshua Bloch和Neal Gafter的Java Puzzlers包括以下变体:
这是一个合法的Java程序吗? 如果是这样,它打印什么?
\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020 \u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079 \u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020 \u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063 \u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028 \u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020 \u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b \u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074 \u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020 \u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b \u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d
(这个程序原来是一个普通的“Hello World”程序。)
在解决这个问题的时候,他们指出了以下几点:
更严重的是,这个难题有助于强化前面三个方面的教训: 当你需要插入不能以任何其他方式表示的字符到你的程序中时,Unicode转义是非常重要的。 在所有其他情况下避免它们。
来源: Java:在评论中执行代码?
由于这还没有解决,在这里解释,为什么Unicode转义的翻译发生在任何其他源代码处理之前:
它背后的想法是,它允许在不同的字符编码之间对Java源代码进行无损的转换。 今天,Unicode已经有了广泛的支持,这看起来不是什么问题,但是在那个时候,西方国家的开发者不容易从他的亚洲同事那里得到一些包含亚洲字符的源代码,包括编译和testing)并将结果发回,而不会损坏某些东西。
所以,Java源代码可以用任何编码来编写,并允许标识符,字符和String
文字和注释中的大量字符。 然后,为了无损传输,目标编码不支持的所有字符都被Unicode转义replace。
这是一个可逆的过程,有趣的一点是,翻译可以通过一个工具完成,该工具不需要知道关于Java源代码语法的任何内容,因为翻译规则不依赖于它。 这在编译器内部转换为其实际的Unicode字符时独立于Java源代码语法。 这意味着您可以在两个方向上执行任意数量的翻译步骤,而不会改变源代码的含义。
这是另一个奇怪的function,甚至没有提到的原因: \uuuuuuxxxx
语法:
当翻译工具转义字符并遇到已经是转义序列的序列时,它应该在序列中插入一个附加的u
,将\ucafe
转换为\uucafe
。 含义不会改变,但是当转换到另一个方向时,工具应该只删除一个u
并用Unicode字符replace只包含单个u
的序列。 这样,即使Unicode转义仍然保持原来的forms来回转换。 我猜,没有人使用过这个function
我将完全无效地加上这一点,就是因为我不能自救,而且我还没有看到这个问题,所以这个问题是无效的,因为它包含一个隐藏的前提,那就是错误的,即代码是在一个评论!
在Java源代码中,\ u000d在各个方面都与ASCII CR字符相同。 无论它发生在哪里,它都是一条简单而简单的结尾。 问题中的格式是误导性的,那么这个字符序列实际上在语法上对应的是:
public static void main(String... args) { // The comment below is no typo. // System.out.println("Hello World!"); }
因此,恕我直言,最正确的答案是:代码执行,因为它不在评论; 它在下一行。 Java中不允许执行“注释中的代码”,就像您期望的那样。
大部分的困惑源于这样一个事实:语法荧光笔和IDE不够复杂,不能考虑到这种情况。 他们要么根本不处理unicode转义,要么在parsing代码之后而不是像javac
那样做。
\u000d
转义会终止注释,因为\u
转义在程序被标记之前会统一转换为相应的Unicode字符。 您可以同样使用\u0057\u0057
而不是//
开始评论。
这是您的IDE中的一个错误,应该用语法高亮显示该行,以清楚说明\u000d
结束注释。
这也是该语言的devise错误。 现在不能纠正,因为那会破坏依赖它的程序。 \u
转义应该被编译器转换成相应的Unicode字符,只有在“有意义”的情况下(string文字和标识符,可能还有其他地方),或者它们应该被禁止在U + 0000- 007F范围,或两者兼而有之。 这两种语义中的任何一种都可以防止注释被\u000d
转义终止,而不会干扰\u
转义是有用的情况 – 注意这包括在注释中使用\u
转义作为在非注释中对注释进行编码的方式-Latin脚本,因为文本编辑器可以比编译器更广泛地查看\u
转义位置。 (我不知道任何编辑器或IDE将显示\u
转义为相应的字符在任何情况下,虽然)。
在C系列中有一个类似的devise错误,其中在确定注释边界之前处理反斜线 – 换行符,例如
// this is a comment \ this is still in the comment!
我提出这个问题来说明,这个特定的devise错误很容易发生,而且,如果你习惯于考虑标记和parsing编译器程序员的思维方式关于标记和parsing。 基本上,如果你已经定义了你的forms语法,然后有人提出了一个句法特殊的情况 – trigraphs,backslash-newline,在源文件中编码任意的Unicode字符限于ASCII,无论 – 需要被楔入,更容易在标记器之前添加一个转换通道,而不是重新定义标记器以注意使用该特殊情况的意义。
1对于书呆子:我知道C的这个方面是100%故意的,理由 – 我没有这样做 – 它会允许你用任意长的代码机械地强制代码到打孔的卡片上。 这仍然是一个不正确的devise决定。
这是一个有意的deviseselect,一直回到Java的原始devise。
对于那些问“谁要在注释中使用Unicode转义的人?”的人来说,我认为他们是母语使用拉丁字符集的人。 换句话说,在Java的原始devise中,人们可以在Java程序中任何合法的地方使用任意的Unicode字符,最典型的是在注释和string中。
这可以说是程序(如IDE)中用于查看源文本的一个缺点,即这些程序无法解释Unicode转义并显示相应的字形。
我同意@zwol这是一个devise错误; 但我更批评它。
\u
转义在string和char文字中很有用; 这是它应该存在的唯一的地方。 它应该像其他转义一样处理,如\n
; 和"\u000A"
应该完全是"\n"
。
在评论中绝对没有任何意义 – 没有人可以阅读。
同样,在程序的其他部分使用\uxxxx
也没有意义。 唯一的例外可能是在被强制包含一些非ASCII字符的公共API中 – 我们最后一次看到的是什么?
1995年的devise师有他们的理由,但20年后,这似乎是一个错误的select。
(问题给读者 – 为什么这个问题不断得到新的选票?这个问题是从一个受欢迎的地方连接起来的)
唯一可以回答为什么Unicode转义符被实现的人是编写规范的人。
一个可能的原因是,有人希望允许整个BMP作为Java源代码的可能字符。 但是这提出了一个问题:
- 你想能够使用任何BMP字符。
- 你想能够input任何BMP字符相当容易。 一种方法是使用Unicode转义符。
- 您希望保持词汇规范易于人类阅读和书写,并且相当容易实施。
当Unicode转义input时,这是非常困难的:它创build了一个新的词法分析规则。
简单的解决方法是分两步进行search:首先search所有的Unicode转义符并将其replace为所代表的字符,然后parsing生成的文档,就好像Unicode转义符不存在一样。
这个好处是指定起来很简单,所以它使规格更简单,而且易于实现。
缺点是,你的榜样。
编译器不仅将Unicode转义符转换为它们表示的字符,然后才将程序parsing为令牌,而是在丢弃注释和空白之前这样做。
该程序包含一个唯一的Unicode转义(\ u000d),位于其唯一的注释中。 正如注释告诉你的,这个转义代表了换行字符,编译器在丢弃注释之前正确地翻译它 。
它是依赖于平台的,在某些平台上,比如UNIX,它会在其他的平台上工作,比如Windows,它不会。 虽然输出可能与肉眼看起来相同,但是如果将其保存在文件中,或者通过pipe道连接到另一个程序进行后续处理,则很容易造成问题。