题 为什么在允许某些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规范允许这样做?


1247
2018-06-09 09:02


起源


“为什么这是允许的”似乎对我来说过于舆论。语言设计师做出了决定,还有什么需要知道的?除非您找到做出该决定的人的陈述,否则我们只能推测。 - Ingo Bürk
有趣的是至少有一件事 OP的IDE显然错了 并显示不正确的突出显示, - dhke
可能相关: stackoverflow.com/questions/4448180/... - dhke
@Tobb但Java设计师 正在访问SO 所以它是 可能 得到其中一个人的答案。他们也可能存在已经回答这个问题的资源。 - Pshemo
简单的答案是,根据语言规则,代码根本不在评论中,因此问题是不正确的。 - user207421


答案:


Unicode解码在任何其他词汇翻译之前进行。这样做的主要好处是可以在ASCII和任何其他编码之间来回切换。你甚至不需要弄清楚评论的开始和结束位置!

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

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

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

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

关于这个主题有许多问题 Java Puzzlers 作者Joshua Bloch和Neal Gafter包括以下变体:

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


687
2018-06-09 09:13



简而言之,Java有意允许它:“bug”在OP的IDE中? - Bathsheba
@Bathsheba:这更像是人们的头脑。人们不会试图理解Java解析的工作原理,因此IDE有时会以错误的方式显示代码。在上面的示例中,注释应该以。结尾 \u000d 而它后面的部分应该有代码亮点。 - Aaron Digulla
另一个常见的错误是在代码中粘贴Windows路径 // C:\user\... 从那导致编译错误 \user 不是有效的Unicode转义序列。 - Aaron Digulla
在日食之后 \u000d 部分突出显示。按Ctrl + Shift + F后,字符将替换为新行,其余行将被换行 - bluelDe
@TheLostMind如果我正确理解了答案你也应该能够用块注释重现它。 \u002A/ 应结束评论。 - Taemyr


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

其背后的想法是它允许在不同的字符编码之间无损翻译Java源代码。今天,有广泛的Unicode支持,这看起来不是一个问题,但是当时西方国家的开发人员从包含亚洲字符的亚洲同事那里收到一些源代码并不容易做出一些改变(包括编译和测试它并将结果发回,所有这些都不会损坏。

因此,Java源代码可以用任何编码编写,并允许标识符,字符和标识符中的各种字符 String文字和评论。然后,为了无损地传输它,目标编码不支持的所有字符都被它们的Unicode转义替换。

这是一个可逆的过程,有趣的是,转换可以通过一个工具完成,该工具不需要了解Java源代码语法的任何信息,因为转换规则不依赖于它。这适用于编译器内部实际Unicode字符的转换也独立于Java源代码语法。这意味着您可以在两个方向上执行任意数量的转换步骤,而无需更改源代码的含义。

这就是另一个奇怪的特征,甚至没有提到的原因: \uuuuuuxxxx 句法:

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


132
2018-06-09 17:59



有趣的是, native2ascii 似乎没有使用 \uu...xxxx 句法, - ninjalj
是啊, native2ascii 旨在通过将资源包转换为iso-latin-1来帮助准备资源包 Properties.load 固定只读拉丁-1。在那里,规则是不同的,不 \uuu… 语法并没有早期处理阶段。在属性文件中, property=multi\u000aline 确实是一样的 property=multi\nline。 (与文档中“使用Java语言规范第3.3节中定义的Unicode转义”这一短语相矛盾) - Holger
请注意,这个设计目标可以在没有任何瑕疵的情况下实现;最简单的方法就是禁止 \u 转义以生成U + 0000-007F范围内的字符。 (所有这些字符都可以通过20世纪90年代相关的所有国家编码本地表示 - 好吧,也许除了一些控制字符,但是你无论如何都不需要那些编写Java。) - zwol
@zwol:好吧,如果你排除了Java源代码中不允许的控制字符,那你就是对的。然而,这意味着制定规则更加复杂。今天,讨论这个决定为时已晚...... - Holger
啊在utf8中保存文档而不是拉丁语或其他东西的问题。由于这种西方的废话,我的所有数据库都被打破了 - David 天宇 Wong


我将完全无效地添加这一点,仅仅是因为我无法帮助自己,我还没有看到它,但问题是无效的,因为它包含一个错误的隐藏前提,即代码在一条评论!

在Java源代码中,\ u000d在各方面都与ASCII CR字符等效。无论它出现在哪里,它都是一个简单明了的行。问题中的格式是误导性的,字符序列实际上在语法上对应的​​是:

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

恕我直言,最正确的答案是:代码执行,因为它不在评论中;它在下一行。 Java中不允许“在注释中执行代码”,就像您期望的那样。

大部分混淆源于语法高亮显示器和IDE不够复杂以考虑这种情况。它们或者根本不处理unicode转义,或者它们在解析代码之后而不是之前执行它,比如 javac 确实。


97
2018-06-10 17:37



我同意,这不是java“设计错误”,但它是一个IDE错误。 - bvdb
问题在于为什么代码会这样 容貌 就像对不熟悉语言这个特定方面的人的评论,也许没有提到语法高亮,实际上就是这样 不 一条评论。在无效问题的前提下反对是不诚实的。 - Phil


\u000d escape终止了评论,因为 \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!

我提出这个问题来说明碰巧很容易发生这个特定的设计错误,并且如果你习惯于考虑标记化和解析编译程序员的思维方式,那么直到修正它为时已经太晚才会发现它是错误的。关于标记化和解析。基本上,如果你已经定义了你的正式语法,然后有人提出了一个语法特殊情况 - trigraphs,反斜杠 - 换行符,在源文件中编码任意Unicode字符,限制为ASCII,无论什么 - 需要楔入,它更容易添加转换通道 之前 令牌化器比重新定义标记器以注意使用该特殊情况有意义的地方。

1 对于学龄儿童:我知道C的这个方面是100%故意的,理由是 - 我没有这样做 - 它可以让你用任意长线机械强制编码到打孔卡片上。这仍然是一个不正确的设计决定。


63
2018-06-09 15:16



我不会说这是一个设计 错误。我同意你的意见,这是一个糟糕的设计选择,或者是一个带有不幸后果的选择,但我仍然认为它可以像语言设计者那样工作:它使你能够在文件的任何地方使用任何unicode字符,同时保持ASCII编码的文件。 - aioobe
那已经说过,我认为选择加工阶段 \u 与使用前导零点进行八进制表示法的C领先决定相比,这更不荒谬了。虽然八进制符号有时是有用的,但我还没有听到任何人说出为什么一个前导零是指示它的好方法。 - supercat
@supercat将该功能引入C89的人们推广了原始K&R预处理器的行为,而不是从头开始设计功能。我怀疑他们是否熟悉穿孔卡的最佳实践,我也怀疑这个功能有没有 曾经 被用于其声明的目的,除了一两次逆向计算练习。 - zwol
@supercat我不会遇到Java问题 \u 如果禁止在U + 0000..U + 007F范围内生成字符,则作为预标记化转换。这是“这无处不在”和“这个具有语法意义的ASCII字符别名”的组合,将其从尴尬变为扁平错误。 - zwol
在你的“为学生”:当然在那个时候 该 // 单行评论不存在。并且由于C有一个不是新行的语句终结符,它主要用于长字符串,除了我可以确定“字符串文字串联” 是 来自K&R。 - Mark Hurd


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

对于那些问“谁想要在评论中逃脱Unicode?”的人,我认为他们是那些母语使用拉丁字符集的人。换句话说,在Java的原始设计中,人们可以在Java程序中的任何合法地方使用任意Unicode字符,最常见的是在注释和字符串中。

可以说,用于查看源文本的程序(如IDE)的缺点是这些程序无法解释Unicode转义并显示相应的字形。


21
2018-06-09 18:45



现在我们使用UTF-8作为源代码,可以直接使用Unicode字符,不需要转义。 - Paŭlo Ebermann


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

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

绝对没有意义 \uxxxx 在评论中 - 没有人能读到这一点。

同样,没有必要使用 \uxxxx 在该计划的其他部分。唯一的例外可能是公共API被强制包含一些非ascii字符 - 我们最后一次看到它是什么?

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

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


21
2018-06-09 16:47



我想,你不会闲逛,在API中使用非ASCII字符。有人使用它(不是我),例如在亚洲国家。当您在标识符中使用非ASCII字符时,禁止在文档注释中使用它们毫无意义。然而,允许它们在令牌内并允许它们改变令牌的含义或边界是不同的事情。 - Holger
他们可以使用适当的文件编码。为什么写 int \u5431 什么时候可以做 int 整 - ZhongYu
你什么时候做 您 必须针对其API编译代码,并且不能使用正确的编码(假设没有广泛使用 UTF-8 支持1995年)。您只需调用一个方法,并且不希望为该单个方法安装操作系统的亚洲语言支持包(请记住,九十年代)... - Holger
现在比1995年更清楚的是,如果你想编程,你最好懂英语。编程是一种国际互动,几乎所有资源都是英文的。 - ZhongYu
我认为这没有改变。 Java的文档大部分时间都是全英文的。有一段日语翻译保持了一段时间,但保持 二 语言并没有真正支持为世界上所有语言环境维护它的想法(它反而反驳了它)。在此之前,无论如何都没有标识符支持Unicode的主流语言。有人说,我猜 思想 本地化的源代码是下一个重要的事情。我会说 感激地它没有起飞。 - Holger


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

一个似是而非的理由是,希望允许整个BMP成为Java源代码的可能字符。这提出了一个问题:

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

当Unicode转义进入战斗时,这是非常困难的:它创建了一整套新的词法分析器规则。

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

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

不好的是,你的榜样。


11
2018-06-12 11:59



或者,将\ uxxxx的使用限制为标识符,字符串文字和字符常量。这是C11的作用。 - ninjalj
这确实使解析器规则复杂化,因为那些是定义那些东西的东西,这就是我推测的原因之一就是它的原因。 - Martijn


编译器不仅会将Unicode转义转换为它们在将程序解析为令牌之前所代表的字符,但它会在丢弃注释和空格之前执行此操作。

该程序包含一个Unicode转义符(\ u000d),位于其唯一注释中。正如评论告诉你的那样,这个转义表示换行字符,编译器正式转换它 在放弃评论之前

这与平台有关。在某些平台上,例如UNIX,它可以工作;在其他方面,例如Windows,它不会。虽然肉眼可能看起来相同,但如果将其保存在文件中或通过管道传输到另一个程序进行后续处理,则很容易引起问题。


1
2017-11-02 13:01



尽管你的“答案”可能很有说服力,但实际上根本不是答案。 OP的问题是“为什么允许这样做”,但这是对它如何运作的解释......哪个OP已经提供了。 - mmgross
您是否有任何消息来源确认这是依赖于平台的?如果这是真的,我会认为Java完全被破坏了(无论如何,这只是棺材中的另一个钉子)。 - Clearer