为什么允许在带有某些 Unicode 字符的注释中执行 Java 代码?
Why is executing Java code in comments with certain Unicode characters allowed?
下面的代码产生输出 "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!");
}
因此导致评论为 "executed"。
既然这可以用于 "hide" 恶意代码或任何邪恶的程序员可以想到的东西,为什么在评论中允许它?
为什么 Java 规范允许这样做?
Unicode 解码发生在任何其他词汇翻译之前。这样做的主要好处是它使得在 ASCII 和任何其他编码之间来回切换变得微不足道。您甚至不需要弄清楚评论的开始和结束位置!
如 JLS Section 3.3 中所述,这允许任何基于 ASCII 的工具处理源文件:
[...] The Java programming language specifies a standard way of transforming a program written in Unicode into ASCII that changes a program into a form that can be processed by ASCII-based tools. [...]
这为平台独立性(支持的字符集的独立性)提供了根本保证,这一直是Java平台的一个关键目标。
能够在文件中的任何位置编写任何 Unicode 字符是一个巧妙的功能,在用非拉丁语言记录代码时,在注释中尤其重要。它可以以如此微妙的方式干扰语义的事实只是一个(不幸的)副作用。
这个主题有很多陷阱,Joshua Bloch 和 Neal Gafter 的 Java Puzzlers 包括以下变体:
Is this a legal Java program? If so, what does it print?
\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"程序。)
在解谜题中,他们指出了以下几点:
More seriously, this puzzle serves to reinforce the lessons of the previous three: Unicode escapes are essential when you need to insert characters that can’t be represented in any other way into your program. Avoid them in all other cases.
\u000d
转义终止注释,因为 \u
转义在 程序被标记化之前被统一转换为相应的 Unicode 字符 。您同样可以使用 \u0057\u0057
而不是 //
来 开始 评论。
这是您 IDE 中的一个错误,它应该语法高亮该行以明确 \u000d
结束评论。
这也是语言的设计错误。现在无法更正它,因为那会破坏依赖它的程序。 \u
转义应仅在“有意义”的上下文中(字符串文字和标识符,可能在其他任何地方)由编译器转换为相应的 Unicode 字符,或者应该禁止它们在 U 中生成字符+0000–007F 范围,或两者。这些语义中的任何一个都会阻止注释被 \u000d
转义终止,而不会干扰 \u
转义有用的情况——请注意 包含 在注释中使用 \u
转义作为一种在非拉丁脚本中对注释进行编码的方式,因为文本编辑器可以比编译器更广泛地了解 \u
转义的重要性。 (不过,我不知道有任何编辑器或 IDE 会将 \u
转义显示为 any 上下文中的相应字符。)
C 系列中也存在类似的设计错误,1 在确定注释边界之前处理反斜杠换行符,例如
// this is a comment \
this is still in the comment!
我提出这个是为了说明,如果您习惯于考虑标记化和解析,那么很容易犯这个特定的设计错误,并且直到为时已晚而无法纠正它时才意识到这是一个错误编译器程序员考虑标记化和解析的方式。基本上,如果您已经定义了形式语法,然后有人想出了一个句法特例——三字母、反斜杠换行符、在源文件中对任意 Unicode 字符进行编码,但仅限于 ASCII,无论如何——需要插入,这样更容易添加转换传递 before tokenizer 而不是重新定义 tokenizer 以注意在何处使用该特殊情况。
1 对于书呆子:我知道 C 的这一方面是 100% 有意为之的,其基本原理——我不是编造的——它会让你机械地将带有任意长行的代码强制安装到穿孔卡片上。这仍然是一个错误的设计决定。
我同意@zwol 的观点,这是一个设计错误;但我更批评它。
\u
转义在字符串和字符文字中很有用;那是它唯一应该存在的地方。它的处理方式应与 \n
等其他转义方式相同;和 "\u000A"
应该 正好意味着 "\n"
.
在评论中加入 \uxxxx
绝对没有意义 - 没有人可以阅读。
同样,在程序的其他部分使用 \uxxxx
也没有意义。唯一的例外可能是 public API 被强制包含一些非 ascii 字符 - 我们最后一次看到这种情况是什么时候?
设计师在 1995 年有他们的理由,但 20 年后,这似乎是一个错误的选择。
(读者提问 - 为什么这个问题不断获得新的投票?这个问题是从某个流行的地方链接过来的吗?)
由于这还没有解决,这里解释一下,为什么 Unicode 转义的转换发生在任何其他源代码处理之前:
其背后的想法是它允许 Java 源代码在不同字符编码之间进行无损转换。今天,Unicode 得到了广泛的支持,这看起来不是问题,但在当时,来自西方国家的开发人员从他的亚洲同事那里收到一些包含亚洲字符的源代码,进行一些修改并不容易(包括编译和测试它)并将结果发回,所有这些都不会损坏任何东西。
因此,Java 源代码可以用任何编码编写,并允许在标识符、字符和 String
文字和注释中使用各种字符。然后,为了无损传输,目标编码不支持的所有字符都被替换为它们的 Unicode 转义字符。
这是一个可逆的过程,有趣的是翻译可以通过一个不需要知道任何关于 Java 源代码语法的工具来完成,因为翻译规则不依赖于它。这是因为编译器内部对它们实际 Unicode 字符的翻译也独立于 Java 源代码语法。这意味着您可以在两个方向上执行任意数量的翻译步骤,而无需更改源代码的含义。
这是另一个甚至没有提到的奇怪功能的原因:\uuuuuuxxxx
语法:
当翻译工具在转义字符时遇到一个已经是转义序列的序列,它应该在序列中插入一个额外的u
,将\ucafe
转换为\uucafe
。意思没有改变,但是当转换到另一个方向时,该工具应该只删除一个 u
并只用它们的 Unicode 字符替换包含单个 u
的序列。这样,即使 Unicode 转义符在来回转换时也能保留其原始形式。我想,没有人使用过该功能......
这是一个有意的设计选择,可以追溯到 Java 的原始设计。
对于那些问"who wants Unicode escapes in comments?"的人,我认为他们是母语使用拉丁字符集的人。换句话说,在 Java 的原始设计中,人们可以在 Java 程序中合法的任何地方使用任意 Unicode 字符,最典型的是在注释和字符串中。
可以说,用于查看源文本的程序(如 IDE)的一个缺点是此类程序无法解释 Unicode 转义并显示相应的字形。
我将完全无效地添加点,只是因为我无法帮助自己而且我还没有看到它,这个问题是无效的,因为它包含一个隐藏的错误前提,即代码在评论中!
在 Java 源代码中,\u000d 在各个方面都等同于 ASCII CR 字符。无论出现在哪里,它都是一个简单明了的行结尾。问题中的格式具有误导性,该字符序列在语法上实际对应的是:
public static void main(String... args) {
// The comment below is no typo.
//
System.out.println("Hello World!");
}
恕我直言,最正确的答案是:代码执行是因为它不在注释中;它在下一行。 "Executing code in comments" 在 Java 中是不允许的,就像你期望的那样。
很多混淆源于语法高亮器和 IDE 不够复杂,无法考虑这种情况。他们要么根本不处理 unicode 转义,要么在解析代码之后而不是之前处理,就像 javac
那样。
唯一能够回答为什么按原样实现 Unicode 转义的人是编写规范的人。
一个可能的原因是希望允许整个 BMP 作为 Java 源代码的可能字符。但这提出了一个问题:
- 您希望能够使用任何 BMP 字符。
- 您希望能够相当容易地输入任何 BMP 字符。一种方法是使用 Unicode 转义。
- 您希望词法规范易于人们阅读和编写,并且相当容易实现。
当 Unicode 转义出现时,这非常困难:它会创建一大堆新的词法分析器规则。
简单的方法是分两步进行词法分析:首先搜索所有 Unicode 转义字符并将其替换为它所代表的字符,然后像不存在 Unicode 转义字符一样解析生成的文档。
这样做的好处是它很容易指定,因此它使规范更简单,也很容易实现。
缺点是,嗯,你的例子。
“原因是Java编译器将Unicode字符\u000d解析为新行”。
如果为真,那么这正是错误发生的地方。
Java 编译器或许应该拒绝编译此源代码,因为(作为 Java 源代码)它格式错误,因此要么一开始就不好,要么在途中被篡改,要么发生变异通过工具链中不理解转换规则的东西。他们应该而不是盲目改造它。
如果所讨论的编辑器是一个仅限 ASCII 的工具,那么该编辑器正在做正确的事情——将 Unicode 转义序列视为(格式错误的)注释中无意义的字符串。
如果有问题的编辑器是一个支持 Unicode 的工具,那么它也是在做正确的事情——“按原样”保留 Unicode 转义序列,并将其视为无意义的字符串-形成)评论。
无损、可逆转换需要将 1-1 映射到的转换——因此两个集合的交集必须为空。即使没有字符被正确实施的转义转换修改,这两个集合也可以重叠,因为范围 (000-07F) 中的转义 Unicode 可能已经存在于输入流中。
如果目标是 Unicode 和 ASCII 之间的无损、可逆转换,则转换 to/from ASCII 的要求是 escape-ify/re-encode 任何大于十六进制 007F 的 Unicode 字符,其余的不用理会。
这样做之后,支持 Unicode 的语言将在注释或字符串以外的任何地方将转义的 Unicode 字符视为错误——它们不得在注释内转换,但必须在字符串内转换--因此,在 词法分析将源转换为标记(即 lexemes)之前,不得进行转换,从而允许以类型安全的方式完成转换。
下面的代码产生输出 "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!");
}
因此导致评论为 "executed"。
既然这可以用于 "hide" 恶意代码或任何邪恶的程序员可以想到的东西,为什么在评论中允许它?
为什么 Java 规范允许这样做?
Unicode 解码发生在任何其他词汇翻译之前。这样做的主要好处是它使得在 ASCII 和任何其他编码之间来回切换变得微不足道。您甚至不需要弄清楚评论的开始和结束位置!
如 JLS Section 3.3 中所述,这允许任何基于 ASCII 的工具处理源文件:
[...] The Java programming language specifies a standard way of transforming a program written in Unicode into ASCII that changes a program into a form that can be processed by ASCII-based tools. [...]
这为平台独立性(支持的字符集的独立性)提供了根本保证,这一直是Java平台的一个关键目标。
能够在文件中的任何位置编写任何 Unicode 字符是一个巧妙的功能,在用非拉丁语言记录代码时,在注释中尤其重要。它可以以如此微妙的方式干扰语义的事实只是一个(不幸的)副作用。
这个主题有很多陷阱,Joshua Bloch 和 Neal Gafter 的 Java Puzzlers 包括以下变体:
Is this a legal Java program? If so, what does it print?
\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"程序。)
在解谜题中,他们指出了以下几点:
More seriously, this puzzle serves to reinforce the lessons of the previous three: Unicode escapes are essential when you need to insert characters that can’t be represented in any other way into your program. Avoid them in all other cases.
\u000d
转义终止注释,因为 \u
转义在 程序被标记化之前被统一转换为相应的 Unicode 字符 。您同样可以使用 \u0057\u0057
而不是 //
来 开始 评论。
这是您 IDE 中的一个错误,它应该语法高亮该行以明确 \u000d
结束评论。
这也是语言的设计错误。现在无法更正它,因为那会破坏依赖它的程序。 \u
转义应仅在“有意义”的上下文中(字符串文字和标识符,可能在其他任何地方)由编译器转换为相应的 Unicode 字符,或者应该禁止它们在 U 中生成字符+0000–007F 范围,或两者。这些语义中的任何一个都会阻止注释被 \u000d
转义终止,而不会干扰 \u
转义有用的情况——请注意 包含 在注释中使用 \u
转义作为一种在非拉丁脚本中对注释进行编码的方式,因为文本编辑器可以比编译器更广泛地了解 \u
转义的重要性。 (不过,我不知道有任何编辑器或 IDE 会将 \u
转义显示为 any 上下文中的相应字符。)
C 系列中也存在类似的设计错误,1 在确定注释边界之前处理反斜杠换行符,例如
// this is a comment \
this is still in the comment!
我提出这个是为了说明,如果您习惯于考虑标记化和解析,那么很容易犯这个特定的设计错误,并且直到为时已晚而无法纠正它时才意识到这是一个错误编译器程序员考虑标记化和解析的方式。基本上,如果您已经定义了形式语法,然后有人想出了一个句法特例——三字母、反斜杠换行符、在源文件中对任意 Unicode 字符进行编码,但仅限于 ASCII,无论如何——需要插入,这样更容易添加转换传递 before tokenizer 而不是重新定义 tokenizer 以注意在何处使用该特殊情况。
1 对于书呆子:我知道 C 的这一方面是 100% 有意为之的,其基本原理——我不是编造的——它会让你机械地将带有任意长行的代码强制安装到穿孔卡片上。这仍然是一个错误的设计决定。
我同意@zwol 的观点,这是一个设计错误;但我更批评它。
\u
转义在字符串和字符文字中很有用;那是它唯一应该存在的地方。它的处理方式应与 \n
等其他转义方式相同;和 "\u000A"
应该 正好意味着 "\n"
.
在评论中加入 \uxxxx
绝对没有意义 - 没有人可以阅读。
同样,在程序的其他部分使用 \uxxxx
也没有意义。唯一的例外可能是 public API 被强制包含一些非 ascii 字符 - 我们最后一次看到这种情况是什么时候?
设计师在 1995 年有他们的理由,但 20 年后,这似乎是一个错误的选择。
(读者提问 - 为什么这个问题不断获得新的投票?这个问题是从某个流行的地方链接过来的吗?)
由于这还没有解决,这里解释一下,为什么 Unicode 转义的转换发生在任何其他源代码处理之前:
其背后的想法是它允许 Java 源代码在不同字符编码之间进行无损转换。今天,Unicode 得到了广泛的支持,这看起来不是问题,但在当时,来自西方国家的开发人员从他的亚洲同事那里收到一些包含亚洲字符的源代码,进行一些修改并不容易(包括编译和测试它)并将结果发回,所有这些都不会损坏任何东西。
因此,Java 源代码可以用任何编码编写,并允许在标识符、字符和 String
文字和注释中使用各种字符。然后,为了无损传输,目标编码不支持的所有字符都被替换为它们的 Unicode 转义字符。
这是一个可逆的过程,有趣的是翻译可以通过一个不需要知道任何关于 Java 源代码语法的工具来完成,因为翻译规则不依赖于它。这是因为编译器内部对它们实际 Unicode 字符的翻译也独立于 Java 源代码语法。这意味着您可以在两个方向上执行任意数量的翻译步骤,而无需更改源代码的含义。
这是另一个甚至没有提到的奇怪功能的原因:\uuuuuuxxxx
语法:
当翻译工具在转义字符时遇到一个已经是转义序列的序列,它应该在序列中插入一个额外的u
,将\ucafe
转换为\uucafe
。意思没有改变,但是当转换到另一个方向时,该工具应该只删除一个 u
并只用它们的 Unicode 字符替换包含单个 u
的序列。这样,即使 Unicode 转义符在来回转换时也能保留其原始形式。我想,没有人使用过该功能......
这是一个有意的设计选择,可以追溯到 Java 的原始设计。
对于那些问"who wants Unicode escapes in comments?"的人,我认为他们是母语使用拉丁字符集的人。换句话说,在 Java 的原始设计中,人们可以在 Java 程序中合法的任何地方使用任意 Unicode 字符,最典型的是在注释和字符串中。
可以说,用于查看源文本的程序(如 IDE)的一个缺点是此类程序无法解释 Unicode 转义并显示相应的字形。
我将完全无效地添加点,只是因为我无法帮助自己而且我还没有看到它,这个问题是无效的,因为它包含一个隐藏的错误前提,即代码在评论中!
在 Java 源代码中,\u000d 在各个方面都等同于 ASCII CR 字符。无论出现在哪里,它都是一个简单明了的行结尾。问题中的格式具有误导性,该字符序列在语法上实际对应的是:
public static void main(String... args) {
// The comment below is no typo.
//
System.out.println("Hello World!");
}
恕我直言,最正确的答案是:代码执行是因为它不在注释中;它在下一行。 "Executing code in comments" 在 Java 中是不允许的,就像你期望的那样。
很多混淆源于语法高亮器和 IDE 不够复杂,无法考虑这种情况。他们要么根本不处理 unicode 转义,要么在解析代码之后而不是之前处理,就像 javac
那样。
唯一能够回答为什么按原样实现 Unicode 转义的人是编写规范的人。
一个可能的原因是希望允许整个 BMP 作为 Java 源代码的可能字符。但这提出了一个问题:
- 您希望能够使用任何 BMP 字符。
- 您希望能够相当容易地输入任何 BMP 字符。一种方法是使用 Unicode 转义。
- 您希望词法规范易于人们阅读和编写,并且相当容易实现。
当 Unicode 转义出现时,这非常困难:它会创建一大堆新的词法分析器规则。
简单的方法是分两步进行词法分析:首先搜索所有 Unicode 转义字符并将其替换为它所代表的字符,然后像不存在 Unicode 转义字符一样解析生成的文档。
这样做的好处是它很容易指定,因此它使规范更简单,也很容易实现。
缺点是,嗯,你的例子。
“原因是Java编译器将Unicode字符\u000d解析为新行”。
如果为真,那么这正是错误发生的地方。
Java 编译器或许应该拒绝编译此源代码,因为(作为 Java 源代码)它格式错误,因此要么一开始就不好,要么在途中被篡改,要么发生变异通过工具链中不理解转换规则的东西。他们应该而不是盲目改造它。
如果所讨论的编辑器是一个仅限 ASCII 的工具,那么该编辑器正在做正确的事情——将 Unicode 转义序列视为(格式错误的)注释中无意义的字符串。
如果有问题的编辑器是一个支持 Unicode 的工具,那么它也是在做正确的事情——“按原样”保留 Unicode 转义序列,并将其视为无意义的字符串-形成)评论。
无损、可逆转换需要将 1-1 映射到的转换——因此两个集合的交集必须为空。即使没有字符被正确实施的转义转换修改,这两个集合也可以重叠,因为范围 (000-07F) 中的转义 Unicode 可能已经存在于输入流中。
如果目标是 Unicode 和 ASCII 之间的无损、可逆转换,则转换 to/from ASCII 的要求是 escape-ify/re-encode 任何大于十六进制 007F 的 Unicode 字符,其余的不用理会。
这样做之后,支持 Unicode 的语言将在注释或字符串以外的任何地方将转义的 Unicode 字符视为错误——它们不得在注释内转换,但必须在字符串内转换--因此,在 词法分析将源转换为标记(即 lexemes)之前,不得进行转换,从而允许以类型安全的方式完成转换。