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
能够接受目前被拒绝的程序,但不会拒绝目前被接受的程序。