为什么在带有某些Unicode字符的注释中执行Java代码?


1356

以下代码产生输出“ 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规范允许这样做?


44
对我来说,“为什么允许这样做”似乎太基于观点了。语言设计师做出了决定,还有什么需要知道的?除非您找到做出该决定的人的声明,否则我们只能推测。
IngoBürk2015年

194
一件有趣的事情是,至少OP的IDE显然会出错并显示不正确的突出显示,
dhke 2015年


47
@Tobb但是Java设计师正在访问SO,因此可以通过其中之一获得答案。同样,它们可能存在已经回答了这个问题的资源。
Pshemo

41
简单的答案是,根据语言规则,代码根本不在注释中,因此问题是不正确的。
user207421 2015年

Answers:


741

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:在注释中执行代码?


84
简而言之,Java特意允许它:“错误”在OP的IDE中?
Bathsheba 2015年

60
@Bathsheba:更多是在人们的头脑中。人们不会试图理解Java解析的工作原理,因此IDE有时会以错误的方式显示代码。在上面的示例中,注释应以结尾,\u000d其后的部分应具有代码突出显示。
亚伦·迪古拉

62
另一个常见的错误是将Windows路径粘贴到类似的代码中// C:\user\...,这导致编译错误,因为\user它不是有效的Unicode转义序列。
亚伦·迪古拉

50
在日食中,之后的代码\u000d部分突出显示。在按Ctrl + Shift + F之后,该字符将替换为新行,并包装其余行
bluelDe

20
@TheLostMind如果我正确理解了答案,那么您也应该能够使用块注释来重现该答案。 \u002A/应该结束评论。
Taemyr 2015年

141

既然尚未解决,请在此处进行解释,解释为什么Unicode转义的转换发生在任何其他源代码处理之前:

其背后的想法是,它允许在不同字符编码之间无损翻译Java源代码。如今,已经有广泛的Unicode支持,这似乎不成问题,但是那时,来自西方国家的开发人员从他的亚洲同事那里收到一些包含亚洲字符的源代码并不容易,需要进行一些更改( (包括对其进行编译和测试)并将结果发送回去,而不会损坏任何内容。

因此,Java源代码可以以任何编码形式编写,并且允许在标识符,字符,String文字和注释中使用多种字符。然后,为了无损地传输它,所有目标编码不支持的字符都将被其Unicode转义符代替。

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

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

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


1
有趣的是,native2ascii似乎没有使用\uu...xxxx语法
ninjalj

5
是的,native2ascii其目的是通过将资源包转换为iso-latin-1(Properties.load仅固定为读取latin-1)来帮助准备资源包。那里的规则是不同的,没有\uuu…语法,也没有早期处理阶段。在属性文件中,property=multi\u000aline的确与相同property=multi\nline。(与文档中的“使用Java™语言规范的3.3节中定义的Unicode转义”短语相反)
Holger

10
注意,没有任何疣就可以实现该设计目标。最简单的方法是禁止\u转义符生成U + 0000–007F范围内的字符。(所有这些字符都可以用1990年代相关的所有国家编码来本地表示-嗯,也许除了某些控制字符外,但您还是不需要编写Java。)
zwol

3
@zwol:好吧,如果您排除了Java源代码中仍然不允许的控制字符,那您是对的。然而,这意味着使规则更加复杂。而今天,讨论该决定为时已晚……
Holger 2015年

嗯,将文件保存在utf8中而不是拉丁语或其他东西的问题。由于西方的胡言乱语,我所有的数据库也被破坏了
David天宇Wong

106

我将完全无效地添加要点,只是因为我不能自救并且还没有看到问题的答案,所以该问题无效,因为它包含错误的隐藏前提,即代码在一条评论!

在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处理。


6
我同意,这不是Java“设计错误”,但这是IDE错误。
bvdb

3
问题在于,为什么对于那些不熟悉该语言特定方面并且可能不参考语法突出显示的人来说,看起来像是注释的代码实际上不是注释。在问题无效的前提下提出异议是毫无根据的。
菲尔(Phil)

@Phil:仅在使用特定工具查看时,它看起来像是评论,其他人则以其他方式显示。
jmoreno

1
@jmoreno不应该有任何超过一个文本编辑器来读取代码。至少,它违反了最不令人惊讶的原则,即//样式注释一直持续到下一个\ n字符-而不是最终由\ n最终替换的任何其他序列。评论永远不会被剥夺。不良的预处理器。
菲尔

69

所述\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%有意的,其基本原理(我不是在编造这一原理)是,它允许您将带有任意长行的代码机械地强制拟合到打孔卡上。这仍然是一个错误的设计决定。


17
我不会说这是一个设计错误。我可以同意您的看法,这是一个糟糕的设计选择,或者是一个不幸的选择,但是我仍然认为它可以按语言设计人员的意图进行工作:它使您可以在文件中的任何位置使用任何Unicode字符,同时保持ASCII编码文件。
aioobe 2015年

12
话虽如此,我认为对于处理阶段的选择\u比决定遵循C的使用前导零进行八进制表示法的决策要荒谬。尽管八进制表示法有时很有用,但我还没有听到有人清楚说明为什么前导零是指示它的好方法的说法。
2015年

3
@supercat将该功能添加到C89中的人们是在推广原始K&R预处理程序的行为,而不是从头开始设计功能。我怀疑他们熟悉打孔卡的最佳实践,并且我也怀疑该功能是否用于其既定目的,除了可能用于一两次反演练习。
zwol

8
@supercat \u如果禁止Java 生成U + 0000..U + 007F范围内的字符,那么Java 作为预令牌转换将不会有问题。正是“这无处不在”和“此别名具有语法意义的ASCII字符”的组合,将其从笨拙降为完全错误。
zwol

4
在您的“为书呆子”:当然那个时候中//单行注释是不存在的。而且,由于C具有语句终止这不是一个新的生产线,这将主要用于长字符串,但据我可以确定“字符串字面串联” 有从K&R。
Mark Hurd

22

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

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

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


8
如今,我们将UTF-8用作源代码,并且可以直接使用Unicode字符,而无需进行转义。
圣保罗Ebermann

21

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

\u转义在字符串和char文字中很有用;那是它应该存在的唯一地方。应该和其他逃生一样处理\n; 并且"\u000A" 应该确切的意思"\n"

\uxxxx评论绝对没有意义-没有人可以阅读。

同样,\uxxxx在程序的其他部分也没有必要使用。唯一的例外可能是在强制包含一些非ASCII字符的公共API中-我们最近一次看到它是什么?

设计师在1995年有了原因,但是20年后,这似乎是一个错误的选择。

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


5
我想,您不是在API中使用非ASCII字符的地方。例如在亚洲国家,有人使用它(不是我)。而且,当您在标识符中使用非ASCII字符时,在文档注释中禁止它们就没有意义。但是,允许它们位于令牌内部并允许它们更改令牌的含义或边界是不同的事情。
Holger 2015年

15
他们可以使用正确的文件编码。为什么写int \u5431什么时候可以做int 整
ZhongYuyu

3
不得不根据其API编译代码而无法使用正确的编码时,会怎么做(假定UTF-81995年没有广泛的支持)。您只需要调用一种方法,就不想为该单一方法安装操作系统的亚洲语言支持包(请记住,九十年代)…
Holger 2015年

5
现在比1995年清楚的是,如果您想编程的话,您会更好地了解英语。编程是一种国际互动,几乎所有资源都是英语。
羽2015年

8
我认为这没有改变。Java的文档大多数时候也是全英文的。保留了一段时间的日语翻译,但是保留两种语言并不能真正支持在世界所有地区维护日语的想法(而是反对了)。在此之前,还没有主流语言在标识符中支持Unicode。所以我想,有人会认为本地化的源代码是下一件大事。我要庆幸地说,它没有起飞。
Holger 2015年

11

唯一可以回答为什么要执行Unicode转义的人是编写规范的人。

一个合理的原因是,人们希望将整个BMP用作Java源代码的可能字符。但是,这带来了一个问题:

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

当Unicode转义进入竞争时,这是极其困难的:它将创建大量新的词法分析器规则。

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

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

不利的一面是你的榜样。


2
或者,将\ uxxxx的使用限制为标识符,字符串文字和字符常量。C11是做什么的。
ninjalj 2015年

但是,这确实使解析器规则变得复杂,因为这些是定义这些东西的原因,而我推测这正是它的原由。
Martijn 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.