JEP 216:正确处理导入语句
概述
修复 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 的类解析过程的一部分,其中包含确定类的超类型、类型变量和成员。
要查看此过程的实际运作情况,请考虑以下代码:
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 的解析开始。
根据 1a,开始处理 import static P.Outer.Nested.*;,这意味着将查找 P.Outer.Nested 及其传递超类型的成员。
P.Outer.Nested 类的解析开始(静态导入也可以导入继承的类型)
触发解析 P.Outer,由于它已经在进行中,因此被跳过。
类型检查 I(implements 子句)运行时,由于 I 尚未在作用域中,因此无法解析。
import P.Q.* 的解析开始,它将 P.Q 的所有成员类型(包括接口 I)导入到当前文件的作用域中。
P.Outer 和其他类的解析继续进行。
如果导入顺序被交换,那么第 6 步会在第 5 步之前发生,因此 I 会在第 5 步被找到。
上述问题并不是与 import 处理相关的唯一问题。另一个已知的问题是,类的类型参数的边界可能合法地引用其声明类中可能存在的内部类。在某些情况下,这目前会导致无法解析的循环,例如:
package P;
import static P.Outer.Nested.*;
public class Outer {
public static class Nested<T extends I> {
static class I { }
}
}
针对这个问题,设想的解决方案是将现有的 javac 成员解析的第一阶段拆分为三个部分:第一部分将分析包含文件的导入(imports),第二部分将仅构建类/接口层次结构,不涉及任何类型参数、注解等,第三部分则会完整地分析类头(class headers),包括类型参数。
预计这一变化将使 javac 能够接受目前被拒绝的程序,但不会拒绝目前被接受的程序。