下面的代码产生输出“Hello World!”(不,真的,试试看)。

public static void main(String... args) {

   // The comment below is not a typo.
   // \u000d System.out.println("Hello World!");
}

这样做的原因是Java编译器将Unicode字符\u000d解析为一个新行,并转换为:

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字符是一个很好的特性,在用非拉丁语言编写代码时,这在注释中尤其重要。事实上,它能以如此微妙的方式干扰语义只是一个(不幸的)副作用。

关于这个主题有许多陷阱,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解码发生在任何其他词汇翻译之前。这样做的主要好处是,它使得在ASCII和任何其他编码之间来回切换变得很简单。你甚至不需要弄清楚注释在哪里开始和结束!

正如JLS 3.3节所述,这允许任何基于ASCII的工具来处理源文件:

[…Java编程语言指定了一种将用Unicode编写的程序转换为ASCII的标准方法,这种方法可以将程序转换为基于ASCII的工具可以处理的形式。[…]

这为平台独立性(支持字符集的独立性)提供了基本保证,这一直是Java平台的关键目标。

能够在文件的任何位置编写任何Unicode字符是一个很好的特性,在用非拉丁语言编写代码时,这在注释中尤其重要。事实上,它能以如此微妙的方式干扰语义只是一个(不幸的)副作用。

关于这个主题有许多陷阱,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:在注释中执行代码?!

这是一个有意的设计选择,可以一直追溯到Java的原始设计。

对于那些问“谁想在注释中使用Unicode转义?”的人,我假设他们的母语是拉丁字符集。换句话说,在Java的原始设计中,人们可以在Java程序中任何合法的地方使用任意的Unicode字符,最典型的是在注释和字符串中。

这可以说是用于查看源文本的程序(如ide)的一个缺点,即这些程序不能解释Unicode转义并显示相应的字形。

“这样做的原因是Java编译器将Unicode字符\u000d解析为新行”。

如果为真,那么这正是错误发生的地方。

Java编译器也许应该拒绝编译这个源代码,因为(作为Java源代码)它是格式不正确的,因此要么一开始就不好,要么在途中被篡改,要么被不理解转换规则的工具链中的某些东西改变了。他们不应该盲目地改造它。

如果所讨论的编辑器是一个只使用ascii的工具,那么所述编辑器正在做正确的事情——将Unicode转义序列视为(格式错误的)注释中无意义的字符串。

如果所讨论的编辑器是一个支持Unicode的工具,那么它也在做正确的事情——保持Unicode转义序列“原样”,并将其视为(格式错误的)注释中无意义的字符串。

无损可逆转换需要将1-1映射到——因此两个集合的交集必须为空。在这里,即使正确实现的escape-ify-ing转换没有修改字符,这两个问题集也可以重叠,因为范围(000-07F)中的escaping - unicode可能已经出现在输入流中。

如果目标是在Unicode和ASCII之间进行无损、可逆的转换,则转换到/从ASCII的要求是转义/重新编码任何大于hex 007F的Unicode字符,并保留其余的字符。

做到这一点后,Unicode感知的语言将把转义后的Unicode字符视为注释或字符串内部以外的任何地方的错误——它们不能在注释中转换,但必须在字符串中转换——因此在词法分析将源转换为标记(即词素)之前不能进行转换,从而允许以类型安全的方式进行转换。

\u000d转义终止注释,因为\u转义在程序被标记化之前被统一转换为相应的Unicode字符。你同样可以使用\u0057\u0057来代替//来开始注释。

这是IDE中的一个错误,应该用语法高亮显示这一行,以明确表示\u000d结束了注释。

这也是语言上的一个设计错误。现在还不能更正,因为这会破坏依赖于它的程序。\u转义应该被编译器转换为相应的Unicode字符,只有在“有意义”的上下文中(字符串字面量和标识符,可能没有其他地方),或者它们应该被禁止生成u + 0000-007F范围内的字符,或者两者都是。这两种语义都可以防止注释被\u000d转义终止,而不会影响\u转义有用的情况——请注意,这包括在注释中使用\u转义,作为一种在非拉丁脚本中编码注释的方式,因为文本编辑器可以比编译器更广泛地考虑\u转义的重要性。(不过,我不知道有任何编辑器或IDE会在任何上下文中将\u转义显示为相应的字符。)

在C族中也有类似的设计错误,1在注释边界确定之前处理了反斜杠换行符。

// this is a comment \
   this is still in the comment!

我提出这一点是为了说明,如果您习惯于像编译器程序员思考标记化和解析那样思考标记化和解析,那么碰巧很容易犯这种特殊的设计错误,并且直到为时已晚时才意识到这是一个错误。基本上,如果您已经定义了形式语法,然后有人提出了一个语法特殊情况——三字符、反斜杠-换行符、在源文件中编码限制为ASCII的任意Unicode字符,等等——需要插入这些情况,那么在标记器之前添加一个转换传递要比重新定义标记器以注意在哪里使用该特殊情况更容易。

1对于学究:我知道C语言的这方面是100%有意为之的,其基本原理(我不是瞎编的)是允许你机械地将任意长行代码强行装到穿孔卡片上。这仍然是一个不正确的设计决策。

唯一能回答为什么Unicode转义会这样实现的人是编写规范的人。

这样做的一个合理的原因是希望允许整个BMP作为Java源代码的可能字符。但这也带来了一个问题:

您希望能够使用任何BMP字符。 您希望能够相当容易地输入任何BMP字符。一种方法是使用Unicode转义。 您希望保持词汇规范易于人们阅读和编写,并且相当容易实现。

当Unicode escape进入争论时,这是非常困难的:它创建了一大堆新的词法分析器规则。

最简单的方法是分两步进行词法分析:首先搜索并将所有Unicode转义替换为它所代表的字符,然后解析结果文档,就好像Unicode转义不存在一样。

这样做的好处是易于指定,因此使规范更简单,而且易于实现。

缺点是,你的例子。