JEP 216: Process Import Statements Correctly | 正确处理导入语句
摘要
修复 javac,使其能够正确接受和拒绝源代码,而不管 import 语句和 extends、implements 子句的顺序如何。
动机
在某些情况下,javac 会接受一定顺序的源代码,并且拒绝相同的源代码,只是导入语句重新排序(例如 JDK-7177813)。这是错误且令人困惑的。
描述
在编译类时,javac 使用多个阶段。考虑到 import 的处理,有两个重要的阶段:
- 类型解析,遍历提供的抽象语法树(AST)以查找类和接口声明,
- 成员解析,包括:
- (1a)如果
T是顶级类,则处理定义T的源文件中的import语句,并将导入的成员添加到T的作用域中 - (1b)如果
T是嵌套类,则解析直接封装T的类(如果有的话) - (2)对
T的extends/implements子句进行类型检查 - (3)对
T的类型变量进行类型检查
- (1a)如果
上述阶段是 javac 类 "resolution" 过程的一部分,其中包括确定类的超类、类型变量和成员。
为了看到这个过程的实际操作,请考虑以下代码:
package P;
import static P.Outer.Nested.*;
import P.Q.*;
public class Outer {
public static class Nested implements I {
}
}
package P.Q;
public interface I {
}在类型解析阶段,会识别到存在类型 P.Outer、P.Outer.Nested 和 P.Q.I。然后,如果要分析 P.Outer 类,成员解析阶段的工作如下:
开始解析
P.Outer开始处理
import static P.Outer.Nested.*;,按照 1a 进行处理,这意味着查找P.Outer.Nested及其传递的超类的成员。开始解析
P.Outer.Nested类(静态导入也可以导入继承的类型)触发对
P.Outer的解析,但由于已经在进行中,所以跳过此步骤运行对
I(即implements子句)的类型检查,但因为目前还没有在作用域中,所以无法解析I。开始解析
import P.Q.*,将P.Q的所有成员类型(包括接口I)导入到当前文件的作用域中继续解析
P.Outer和其他类
如果交换导入语句的顺序,那么步骤 6 将在步骤 5 之前发生,因此在步骤 5 中会找到 I。
上述问题不是与 import 处理相关的唯一问题。另一个已知问题是类的类型参数的边界可能有效地引用其声明类的可能的内部类。在某些情况下,这可能会导致当前无法解析的循环,例如:
package P;
import static P.Outer.Nested.*;
public class Outer {
public static class Nested<T extends I> {
static class I { }
}
}对该问题的设想解决方案是将现有的 javac 成员解析的第一阶段分为三个阶段:第一个阶段将分析封闭文件的导入语句,第二个阶段仅构建类/接口层次结构,而没有任何类型参数、注解等,第三个阶段将正确分析类头,包括类型参数。
预计此更改将使 javac 能够接受当前被拒绝的程序,但不会拒绝当前被接受的程序。