为什么允许使用某些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解析为新行并转换为:

public static void main(String... args) {
// The comment below is not a typo.//System.out.println("Hello World!");}

从而导致注释被“执行”。

由于这可以用来“隐藏”恶意代码或任何邪恶的程序员可以想象的东西,为什么在评论中允许

为什么Java规范允许这样做?

83224 次浏览

Unicode解码发生在任何其他词法翻译之前。这样做的主要好处是,它使在ASCII和任何其他编码之间来回变得微不足道。您甚至不需要弄清楚注释的开始和结束位置!

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

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

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

能够在文件中的任何位置写入任何Unicode字符是一个简洁的功能,在用非拉丁语言记录代码时,在注释中尤其重要。它可以以如此微妙的方式干扰语义学的事实只是一个(不幸的)副作用。

关于这个主题有很多陷阱,Joshua Bloch和Neal GAfter的Java拼图游戏包括以下变体:

这是一个合法的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:在注释中执行代码?

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

这是您的IDE中的bug,它应该语法突出显示该行以明确\u000d结束注释。

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

在C系列中也有类似的设计错误,1在确定注释边界之前处理反斜杠换行符,因此例如。

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

我提出这个问题是为了说明,如果你习惯于像编译器程序员那样考虑标记化和解析,那么碰巧很容易犯这个特定的设计错误,并且没有意识到这是一个错误,直到纠正它已经太晚了。基本上,如果你已经定义了你的形式语法,然后有人想出了一个语法特例——trigraph s,反斜杠-换行符,在源文件中编码任意Unicode字符,仅限于ASCII,无论什么——需要插入,添加一个转换传递之前给标记器比重新定义标记器更容易,以注意使用该特例的意义。

1对于学究:我知道C语言的这方面是100%有意的,其基本原理——我不是在编造——是它允许你机械地将任意长行的代码强制安装到穿孔卡片上。这仍然是一个错误的设计决定。

我同意@zwol的观点,这是一个设计错误;但我更批评它。

\u转义在字符串和char文字中很有用;这是它应该存在的唯一地方。它应该像\n;和"\u000A"应该等其他转义一样处理。

在评论中有\uxxxx是绝对没有意义的-没有人可以阅读它。

类似地,在程序的其他部分使用\uxxxx也没有意义。唯一的例外可能是在强制包含一些非ascii字符的公共API中——我们上次看到这种情况是什么时候?

设计师在1995年有他们的理由,但20年后,这似乎是一个错误的选择。

(向读者提问-为什么这个问题不断获得新的选票?这个问题是否与某个受欢迎的地方有关?)

由于这还没有解决,这里有一个解释,为什么Unicode转义的翻译发生在任何其他源代码处理之前:

它背后的想法是,它允许在不同字符编码之间无损翻译Java源代码。今天,Unicode得到了广泛的支持,这看起来并不是一个问题,但在当时,来自西方国家的开发人员很难从他的亚洲同事那里收到一些包含亚洲字符的源代码,进行一些更改(包括编译和测试)并将结果发送回来,而不会损坏任何东西。

因此,Java源代码可以用任何编码编写,并允许在标识符、字符和String文字和注释中包含广泛的字符。然后,为了无损传输它,目标编码不支持的所有字符都被它们的Unicode转义替换。

这是一个可逆的过程,有趣的一点是,翻译可以通过一个工具来完成,该工具不需要了解Java源代码语法,因为翻译规则不依赖于它。这是因为在编译器内对其实际Unicode字符的翻译也独立于Java源代码语法发生。这意味着你可以在两个方向上执行任意数量的翻译步骤,而无需改变源代码的含义。

这是另一个甚至没有提到的奇怪功能的原因:\uuuuuuxxxx语法:

当翻译工具在转义字符时,遇到一个已经是转义序列的序列时,它应该在该序列中再插入一个u,将\ucafe转换为\uucafe。意思不变,但是转换为另一个方向时,工具应该删除一个u,只将包含单个u的序列替换为它们的Unicode字符。这样,即使是Unicode转义在来回转换时也会保留原来的形式。我猜,没有人用过那个功能……

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

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

可以说,用于查看源文本的程序(如IDE)存在一个缺点,即此类程序无法解释Unicode转义并显示相应的字形。

我将完全无效地添加这一点,只是因为我不能帮助自己,我还没有看到它,这个问题是无效的,因为它包含一个隐藏的前提,这是错误的,即代码在注释中!

在Java源代码在各方面都等同于ASCII CR字符。它是一个行尾,简单明了,无论它发生在哪里。问题中的格式是误导性的,该字符序列在语法上实际对应的是:

public static void main(String... args) {// The comment below is no typo.//System.out.println("Hello World!");}

因此,最正确的答案是:代码执行是因为它不在注释中;它在下一行。Java不允许“在注释中执行代码”,就像你所期望的那样。

大部分混淆源于这样一个事实,即语法高亮器和IDE不够复杂,无法考虑到这种情况。它们要么根本不处理Unicode转义,要么在解析代码后而不是之前处理,就像javac一样。

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

一个合理的原因是希望允许整个BMP作为Java源代码的可能字符。

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

当Unicode转义进入战斗时,这是非常困难的:它创建了一大堆新的lexer规则。

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

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

缺点是,好吧,你的例子。

“原因是Java编译器将Unicode字符解析为新行”。

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

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

如果有问题的编辑器是一个仅限ASCII的工具,那么该编辑器正在做正确的事情-将Unicode转义序列视为(格式错误的)注释中无意义的字符串。

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

无损、可逆转换需要将1-1映射到上的转换-因此两个集合的交集必须为空。在这里,即使没有字符被正确实现的转义转换修改,这两个集合也可以重叠,因为范围(000-07F)中的转义Unicode可能已经存在于输入流中。

如果目标是在Unicode和ASCII之间进行无损、可逆的转换,则转换为/从ASCII转换的要求是转义/重新编码任何大于十六进制007F的Unicode字符,而不管其余部分。

这样做之后,Unicode感知的语言将把转义的Unicode字符视为注释或字符串内以外的任何地方的错误-它们不能在注释中转换,但必须在字符串中转换-因此,在之后词法分析将源转换为令牌(即词法)之前,转换不得发生,允许以类型安全的方式进行转换。