Java语法难题


大约12年前,我开始以各种功能为Eclipse生态系统做出贡献。迄今为止,最有趣的经历之一是致力于开发人员工具的开发和处理边缘情况,从而使其他人不必费劲。尽管在此期间我辞去了Eclipse提交人的职务,但如今仍作为Gradle Build Tool的一员致力于开发生产力工具。

在Eclipse上工作时,我深切地记得在进行重构和快速修复时在Java Tooling(JDT)的各个部分上工作。毫不奇怪,处理语言密集型部分的工作与其他非平凡算法的工作一样有困难-从“这很容易”过渡到“为什么我凌晨3点开始阅读Java语言规范?”

打开要复制的错误报告:10分钟。处理补丁:2小时。11年后,无论您是否可以完成该修补程序,我们都会对其进行ping操作:无价。我确实错过了通过https://t.co/vpLD73zOlC进行#eclipse #jdt的工作

-本杰明·穆斯卡拉(@bmuskalla)2019年3月27日 使用特定于语言的工具会使您接触到该语言必须提供的各种边缘情况和精致细节。其中一些是众所周知的,通常被视为“不专业”(您好goto)。实际上根本不知道其他人。而且,在所有适当的尊重下,我非常喜欢发现语言语法的极端情况–很多次使我认为自己知道Java语言语法的同事感到困惑。考虑到我喜欢一个很好的谜题(尤其是Java Puzzles),让我们尝试一个谜题,但仅使用Java语法,而不会出现任何运行时行为。

Using Java for Phishing

让我们从关于Java源文件的众所周知的事实开始。您可以在代码的大多数地方使用Unicode。虽然我们不能在类名中使用完整的Unicode,但是您可以添加足够的Unicode来对您的同事进行恶作剧。

作为一个开胃菜,在您的下一个(远程)配对会话中,只需U+037E在代码中插入“希腊问号”(),然后看着您的同事试图找出该简单的分号有什么问题。网络钓鱼电子邮件最常使用此技术,以使URL看起来像真实URL,但实际上却指向一个截然不同的域。

semicolon.png

由于它甚至不编译,因此您的同事很容易识别和修复。让我们开始偷偷摸摸。

猜猜下面的程序打印什么?

public static void main(String[] args) {
    if (1 == 2) { // one down, one to go: \u000a\u007d\u007b
        System.out.println("1 is 2");
    }
}

是的,在帖子的上下文中,您正确地猜测它正在打印“ 1 is 2”。只是...怎么 即使使用Unicode魔术,又如何诱使Java思考1 == 2?内含评论。有什么猜想吗?实际上,它不会改变表达式。在此过程中损坏了以下Unicode字符:

  • \u000a -换行符 \n
  • \u007d-右花括号 }
  • \u007b -开括号 {

因此,我们实际上正在查看的代码是:

public static void main(String[] args) {
   if (1 == 2) {
   }
   {
       System.out.println("1 is 2");
   }
}

有趣的是,大多数程序员在看到此评论时都会怀疑该评论有些可疑。但是缩进该怎么办,使其不再显示在编辑器中呢?

Blocks of Blocks

让我们继续阅读Java语言规范,看看在其中可以找到哪些有趣的语法。

考虑到实现方法的可能性,Java将方法主体定义为包含Block元素:

MethodBody:
  Block
Block:
  { [BlockStatements] }
BlockStatement:
  LocalVariableDeclarationStatement
  ClassDeclaration
  Statement

仔细研究“块”的定义,我们了解到它们可以包含语句(到目前为止非常好),但也可以包含… ClassDeclaration。现在变得有趣了。让我们看看兔子洞有多深。

public void howDeepCanWeGo() {
    class Foo {
        public void hello() {
            class Bar {
                public void helloFromBar() {
                    // You musn't be afraid to dream a little bigger, darling.
                } 
            }
            new Bar().helloFromBar();
        }
    }
    final Foo instance = new Foo();
    instance.hello();
}

有趣的是,尽管乍看之下此功能似乎毫无用处,但这是我过去在实际测试代码中一直使用的唯一功能。当在一个严重依赖反射的框架上工作时,内联类定义非常有用,可以定义要测试的类并使其与测试保持一致。将一堆嵌套类散布在测试旁边的另一种选择是将它们移近测试的一个很好的理由。您可以在JLS 14.3中了解更多有关本地类的怪癖的信息。

This and That

远离类,更接近操作,让我们看一下方法参数。您可能会遇到几次,所以您不能使用与关键字相同的名称。好吧,让我们看看下面的代码片段。

public class KeywordParameter {
    public static void main(String[] args) {
        KeywordParameter someObject = new KeywordParameter();
        someObject.callMe(3);
    }
    public void callMe(KeywordParameter this, int foo) {
        // ...
    }

}

因此,我们正在创建一个新实例,KeywordParameter并callMe在传递int参数时在其上调用方法。但是等等,该方法有两个参数。甚至以一个关键字命名。那甚至不应该编译,对吗?确实可以。查看JLS 8.4方法声明,我们可以找到方法声明的定义。

MethodDeclarator:
  Identifier ( [ReceiverParameter ,] [FormalParameterList] ) [Dims]

我们看到第一个参数是一个特殊的可选参数,不是形式参数列表的一部分。它实际上定义为始终具有名称this:

ReceiverParameter:
  {Annotation} UnannType [Identifier .] this

所谓的“接收器参数”是一个“可选的语法设备”,表示要在其上调用的对象(因此,它实际上与您期望的对象相同this)。它的唯一目的是在源代码中提供(如果需要)以进行注释。假设我们@Immutable在项目中有一个注释,并且由于某种原因,我们要确保我们的IDE(或其他代码分析器)理解this在我们当前的上下文中表示不可变的数据结构。使用显式的接收器参数,我们可以相应地对其进行注释:

public void callMe(@Immutable KeywordParameter this, int foo) { ... }

@Everywhere

谈论注释事物是为了分析代码,为了使以上代码片段正常工作,注释必须针对PARAMETER。您是否曾经查看过注释可以具有的其他目标?经历最常见的情况,没有什么奇怪的:

TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE,TYPE_PARAMETER,MODULE(since Java9)RECORD_COMPONENT(since Java14)。但是有一个不太明显的位置:TYPE_USE。从名称来看,听起来好像可以在使用任何类型的任何地方使用它。让我们尝试在某些地方使用它:

@TypeAnnotationsEverywhere.Immutable // ok, easy, similar to TYPE 
public class TypeAnnotationsEverywhere {
  public void giveMeMoreTypes() throws @Immutable RuntimeException {  // errr what?
     Object foo = new @Immutable Object(); // WHAT??????
  }
  class Foo implements @Immutable Function { ... }

}

确实,使用TYPE_USE允许我们将注释放在最不寻常的位置。JLS 4.11定义了“类型用法”所涵盖的所有斑点。

您知道其中哪一种语法?全部都得到了吗?帖子的代码也可以在GitHub上找到。同时,我仍在我的博物馆中研究有关语言构造的有趣案例,因此,请分享您遇到的任何事情。


原文链接:http://codingdict.com