这是我在网上找到的一些代码:
class M{public static void main(String[]a){System.out.print(new char[]
{'H','e','l','l','o',' ','W','o','r','l','d','!'});}}
此代码将 Hello World!
打印到屏幕上;您可以看到它运行 here。我可以清楚地看到 public static void main
写的,但它是倒退的。这段代码是如何工作的?这甚至如何编译?
编辑:我在 IntellIJ 中尝试了这段代码,它工作正常。但是,由于某种原因,它在 notepad++ 和 cmd 中不起作用。我还没有找到解决方案,所以如果有人这样做,请在下面评论。
M
之后和 []a
之后:fileformat.info/info/unicode/char/202d/index.htm 它被称为 LEFT-TO-RIGHT OVERRIDE
niam diov citats cilbup
听起来像是拉丁谚语。
这里有一些不可见的字符会改变代码的显示方式。在 Intellij 中,这些可以通过将代码复制粘贴到一个空字符串 (""
) 中找到,该字符串用 Unicode 转义符替换它们,删除它们的效果并显示编译器看到的顺序。
这是该复制粘贴的输出:
"class M\u202E{public static void main(String[]a\u202D){System.out.print(new char[]\n"+
"{'H','e','l','l','o',' ','W','o','r','l','d','!'});}} "
源代码字符按此顺序存储,编译器按此顺序处理,但显示方式不同。
注意 \u202E
字符,它是一个从右到左的覆盖,开始一个所有字符都被强制从右到左显示的块,以及 \u202D
,它是一个从左到右的覆盖,开始一个嵌套块,其中所有字符都被强制为从左到右的顺序,覆盖第一个覆盖。
因此,当它显示原始代码时,class M
正常显示,但是\u202E
将所有内容的显示顺序从那里反转到\u202D
,这再次反转了所有内容。 (正式地,从 \u202D
到行终止符的所有内容都被反转了两次,一次是由于 \u202D
而一次是由于 \u202E
而其余的文本被反转了,这就是为什么这个文本出现在中间由于行终止符,下一行的方向性独立于第一行的方向性处理,因此 {'H','e','l','l','o',' ','W','o','r','l','d','!'});}}
正常显示。
有关完整的(极其复杂,几十页长)Unicode 双向算法,请参阅Unicode Standard Annex #9。
由于 Unicode Bidirectional Algorithm,它看起来不同。 Unicode 双向算法使用 RLO 和 LRO 两个不可见字符来更改嵌套在这两个元字符之间的字符的视觉外观。
结果是 视觉上它们看起来顺序相反,但实际字符 在内存中并没有颠倒。您可以分析结果here。 Java 编译器将忽略 RLO 和 LRO,并将它们视为空格,这就是代码编译的原因。
注 1:文本编辑器和浏览器使用此算法来同时直观地显示 LTR 字符(英语)和 RTL 字符(例如阿拉伯语、希伯来语)的字符 - 因此是“双向”的。您可以在 Unicode 的 website 阅读有关双向算法的更多信息。
注意 2:LRO 和 RLO 的确切行为在算法的 Section 2.2 中定义。
M\u202E
和 a\u202D
的词法部分,但这些标识符似乎被视为等同于 M
和 a
。 (JLS 没有很好地解释这一点。)
字符 U+202E
从右到左反映了代码,但它非常聪明。隐藏在 M 开头,
"class M\u202E{..."
我是如何发现这背后的魔力的?
好吧,起初当我看到这个问题时我很难过,“这是一种玩笑,浪费别人的时间”,但后来,我打开了我的 IDE(“IntelliJ”),创建了一个类,然后通过了代码......它编译!所以,我仔细看了一下,发现“public static void”是向后的,所以我带着光标去了那里,擦除了几个字符......然后发生了什么?字符开始向后擦除,所以,我想嗯……很少见……我必须执行它……所以我继续执行程序,但首先我需要保存它……那是我找到了!。我无法保存文件,因为我的 IDE 说某些字符有不同的编码,并指出它在哪里,所以我开始在 Google 中研究可以完成这项工作的特殊字符,就是这样 :)
一点关于
Unicode 双向算法和涉及的 U+202E
,简要说明 explain:
Unicode 标准规定了一种称为逻辑顺序的内存表示顺序。当文本以水平线显示时,大多数脚本从左到右显示字符。但是,有几个脚本(例如阿拉伯语或希伯来语)显示水平文本的自然顺序是从右到左。如果所有文本具有统一的水平方向,则显示文本的顺序是明确的。但是,因为这些从右到左的脚本使用从左到右书写的数字,所以文本实际上是双向的:从右到左和从左到右文本的混合。除了数字,来自英语和其他脚本的嵌入单词也从左到右书写,也产生双向文本。如果没有明确的规范,当文本的水平方向不统一时,在确定显示字符的顺序时可能会出现歧义。本附件描述了用于确定双向 Unicode 文本方向性的算法。该算法扩展了许多现有实现当前使用的隐式模型,并为特殊情况添加了显式格式化字符。在大多数情况下,无需在文本中包含其他信息即可获得正确的显示顺序。但是,在双向文本的情况下,在某些情况下,隐式双向排序不足以产生可理解的文本。为了处理这些情况,定义了一组最小的方向格式字符来控制渲染时字符的顺序。这允许精确控制显示顺序以实现清晰的交换,并确保用于简单项目(如文件名或标签)的纯文本始终可以正确排序以进行显示。
为什么要创建像 this 这样的算法?
比迪算法可以从右到左依次渲染一系列阿拉伯或希伯来字符。
Chapter 3 of the language specification 通过详细描述如何为 Java 程序完成词法转换来提供解释。对这个问题最重要的是:
程序是用 Unicode(第 3.1 节)编写的,但提供了词汇翻译(第 3.2 节),因此 Unicode 转义(第 3.3 节)可用于包含仅使用 ASCII 字符的任何 Unicode 字符。
所以一个程序是用 Unicode 字符编写的,如果文件编码不支持 Unicode 字符,作者可以使用 \uxxxx
转义它们,在这种情况下它被翻译成适当的字符。在这种情况下出现的 Unicode 字符之一是 \u202E
。它在片段中没有直观显示,但如果您尝试切换浏览器的编码,则可能会出现隐藏字符。
因此,词法翻译导致类声明:
class M\u202E{
这意味着类标识符是 M\u202E
。 specification 认为这是一个有效的标识符:
Identifier:
IdentifierChars but not a Keyword or BooleanLiteral or NullLiteral
IdentifierChars:
JavaLetter {JavaLetterOrDigit}
“Java 字母或数字”是 Character.isJavaIdentifierPart(int) 方法返回 true 的字符。
这实际上是因为 Unicode 双向支持。
U+202E 右至左覆盖 U+202D 左至右覆盖
所以,这些是一些棘手的角色。它们实际上是为从右到左的语言支持而定义的。真正的代码是
class M<U+202E>{public static void main(String[]a<U+202D>){System.out.print(new char[]
{'H','e','l','l','o',' ','W','o','r','l','d','!'});}}
(通过粘贴到 cmd.exe 得到这个)。希望这个答案可以帮助您了解它是如何工作的。