
Type Checking (类型检测) 的作用是分析程序在编译或者运行期间,其类型表达是否一致的一个过程。举个例子:如果一个变量被声明为 int 类型,那么他就不能被赋值为实际的值(或者字符串类型、或者其他任何类型)。java 语言的类型检测分为两种:
本文主要介绍静态类型检测。java 语言在编译时会做大量的类型检测,只要你声明了一个变量的类型,编译器将会确保只有相应类型的值可以被赋值给这个变量(或者这个值的类型是变量类型的子类型)。比如,如果你声明了如下变量:
int x;
这里可以确保它只保存 int 值。但是,如果将变量声明为 List,则该变量可能包含列表的子类型,包括 ArrayList、LinkedList 等。
前面提到静态类型检测主要是对类型的检测,而 java 语言中,类型一致表示的是 类全限定名+ClassLoader 一致,所以在做类型检测时就必定会涉及到某些类的 class load 操作。下面我们就从几个方面来分析下类型检测对于类加载的影响。
在 jvm 参数中配置 -verbose:class 可以观察类加载过程
在下面的例子中, Main 执行过程,check 方法没有被调用,但是该方法返回了一个非 ClassA 的类型,也就是类型 ClassB。那么类型检测就要求就提前加载 ClassA 和 ClassB 类型,加以验证,因此加载顺序如下(ClassA --> ClassB --> ClassC --> ClassD)。
public class ClassA {}
public class ClassB extends ClassA{}
public class ClassC {}
public class ClassD {}
public class Main {
static ClassC c;
static {
c = new ClassC();
}
public static void main(String[] args) {
new ClassD();
}
ClassA check(ClassA a) {
return new ClassB();
}
执行查看类加载顺序如下:
[Loaded com.glmapper.bridge.boot.methodreturn.Main from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.methodreturn.ClassA from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.methodreturn.ClassB from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.methodreturn.ClassC from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.methodreturn.ClassD from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
从这里可以看到,静态域不一定会比非静态域先加载,这里就是因为静态检测提前出发了类的加载导致。
先来看下下面这段代码,大家可以想一下类加载顺序是什么样的
public class ClassA {}
public class ClassB extends ClassA{}
public class ClassC {}
public class Main {
static ClassC c;
static {
c = new ClassC();
}
public static void main(String[] args) {
Main main = new Main();
main.m(new ClassB());
}
void m(ClassA a) {}
}
按照我们惯性理解,Main 加载之后,会加载 ClassC,然后再加载 ClassA 和 ClassB。但是事实是这样吗?通过 -verbose:class 参数执行结果如下:
[Loaded com.glmapper.bridge.boot.paramscheck.Main from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.paramscheck.ClassA from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.paramscheck.ClassB from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.paramscheck.ClassC from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
但是从这里看到,Main 执行时, ClassA ,ClassB 先于 ClassC 加载了。原因是类型检测过程中,会一行行先行的看你的代码,在这个场景中,它发现有 m(ClassA a) 方法,但是代码中传入了 ClassB 这个类型,那么在真正运行 main 方法之前,在运行 Main 的 static 块之前,先行加载了 ClassA 和 ClassB 两个类型,然后验证它们之间的关系。所以看到的类加载顺序是 ClassA -> ClassB -> ClassC ,而非我们概念中的 ClassC -> ClassA -> ClassB。
最后一种场景是变量赋值,来看下面的代码片段:
public class ClassA {}
public class ClassB extends ClassA{}
public class ClassC {}
public class ClassD {
ClassA a;
}
public class Main {
static ClassC c;
static {
c = new ClassC();
}
public static void main(String[] args) {
ClassD d = new ClassD();
d.a = new ClassB();
}
}
启动 main 方法时,在 jvm 参数中配置 -verbose:class 来观察类型加载顺序;
[Loaded com.glmapper.bridge.boot.variableassign.Main from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.variableassign.ClassA from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.variableassign.ClassB from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.glmapper.bridge.boot.variableassign.ClassC from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
[Loaded com.glmapper.bridge.boot.variableassign.ClassD from file:/glmapper/Documents/glmapper/glmapper-blog-samples/glmapper-blog-sample-typecheck/target/classes/]
是不是又有点出乎意料呢?类型检测发现 Main 中包含了 d.a = new ClassB() 的语句,其中 d.a 的类型不是 ClassB,因此会先于 main 方法执行以及先于 Main 中的 static 块执行进行加载。类型检测,将类型 ClassA 和 ClassB 的加载“提前”了。
本文主要介绍了静态类型检测对于 Class Loader 加载类顺序的影响,了解此逻辑对于在考虑多 class loader 场景处理问题非常有用,对于常规的类似 ClassCastExcetion, LinkageError 等异常排查有一定的意义。
两年码农退役“老兵”自己创业开的潮牌店,各位看官有兴趣的可以进去看看。店铺直达:
https://item.taobao.com/item.htm?scm=1007.13982.82927.0&id=617476466652&last_time=1588302810&spm=a8919.3000000002019401.0.1