提起Java提供了参数的动态检验,相信很多人对它的理解:仅仅是在执行期检测参数是否合法。常常我们觉得 classloader的加载顺序就应该在执行期。
Java编译器会开启这种静态检测机制,会使用一组类型检测规则来检测Java字节码,检测这些字节码是否符合规则,如果不符合那么将会被拒绝。但是部分的检测还是在编译器的协助下完成的,而这一步要早于真正执行它。
重要的说三遍:
类型检测是早于执行的!
类型检测是早于执行的!
类型检测是早于执行的!
正因为如此,这一步将会诱发类加载过程提前进行,接下来的示例将会颠覆你对Java类加载的认知。
为了进行验证,需要通过命令行参数:-verbose:class 跟踪类的加载卸载
方法接受参数
当出现方法接收的参数在代码中出现了参数类型的子类时,那么参数类型和子类,将会优先于方法所在类进行加载。因为Java需要判定这个方法是否可以接收这个类型,着重点在验证方法参数类型之间的关系。
public class Father {
}
public class Son extends Father {
}
public class Talk {
void say(Father f) {
}
}
public class Main {
public static void main(String[] args) {
Talk test = new Talk();
test.say(new Son());
}
}
一般正常的理解,Java在执行main时:Main->Talk->Father->Son
但是实际上的加载顺序是:Main->Father->Son->Void->Talk
如果改为:test.say(new Father()),此时顺序就正常:Main->Void->Talk->Father
Java先加载了方法的参数类型和返回值类型(Void),然后再加载了Talk类型。 因为我们的代码里面传递给say方法的不是Father类型,从而诱发Java的编译器检查,进而促使了参数类型的提前加载。
成员变量赋值
当一个类的成员变量被赋值一个子类型时,该成员变量的类型和子类型将会优先于成员变量所在类进行加载。实际的加载顺序是这样的:Main->Father->Son->Void->Talk
public class Talk {
Father f = null;
void say(Father f) {
}
}
public class Main {
public static void main(String[] args) {
Talk test = new Talk();
test.f = new Son();
}
}
方法体类型为返回值父类
当一个类中包括的方法,返回的类型是形参的子类时,形参和返回参数的子类型将会提前加载。check方法是不会被调用的,当Main被加载之后,Java观察到方法check返回了一个非A的类型,此时顺序是:Main->Father->Son->Void->Talk
public class Main {
public static void main(String[] args) {
Talk test = new Talk();
}
Father check(Father f) {
return new Son();
}
}
以上三种情况都会触发提前加载,由于Java编译器在进行规则检查时进行了提前加载所致。