查icp备案是什么网站,网站建设文书,网站定制制作,wordpress创建角色3. 编译期处理
什么是语法糖
所谓的 语法糖 #xff0c;其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中#xff0c;自动生成 和转换的一些代码#xff0c;主要是为了减轻程序员的负担#xff0c;算是 java 编译器给我们的一个额外福利#xff08;给…3. 编译期处理
什么是语法糖
所谓的 语法糖 其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中自动生成 和转换的一些代码主要是为了减轻程序员的负担算是 java 编译器给我们的一个额外福利给糖吃
以下代码分析的前提
注意以下代码的分析借助了 javap 工具、idea 的反编译功能、idea 插件、jclasslib 等工具。另外 编译器转换的结果直接就是 class 字节码只是为了便于阅读给出了 几乎等价 的 java 源码方式并不是编译器还会转换出中间的 java 源码切记。
3.1 默认构造器
public class Candy1 {
}编译成class后的代码
public class Candy1 {// 这个无参构造是编译器帮助我们加上的public Candy1() {super(); // 即调用父类 Object 的无参构造方法即调用 java/lang/Object.
init:()V}
}3.2 自动拆装箱
这个特性是 JDK 5 开始加入的 代码片段1
public class Candy2 {public static void main(String[] args) {Integer x 1;int y x;}
}
这段代码在 JDK 5 之前是无法编译通过的必须改写为 代码片段2 :
public class Candy2 {public static void main(String[] args) {Integer x Integer.valueOf(1);int y x.intValue();}
}显然之前版本的代码太麻烦了需要在基本类型和包装类型之间来回转换尤其是集合类中操作的都是包装类型因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编 译阶段被转换为 代码片段2
3.3 泛型集合取值
泛型也是在 JDK 5 开始加入的特性但 java 在编译泛型代码后会执行 泛型擦除 的动作即泛型信息 在编译为字节码之后就丢失了实际的类型都当做了 Object 类型来处理
public class Candy3 {public static void main(String[] args) {ListInteger list new ArrayList();list.add(10); // 实际调用的是 List.add(Object e)Integer x list.get(0); // 实际调用的是 Object obj List.get(int index);}
}所以在取值时编译器真正生成的字节码中还要额外做一个类型转换的操作
// 需要将 Object 转为 Integer
Integer x (Integer)list.get(0)
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x ((Integer)list.get(0)).intValue();
擦除的是字节码上的泛型信息可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息
public cn.itcast.jvm.t3.candy.Candy3();descriptor: ()Vflags: ACC_PUBLICCode:stack1, locals1, args_size10: aload_01: invokespecial #1 // Method java/lang/Object.
init:()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcn/itcast/jvm/t3/candy/Candy3;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack2, locals3, args_size10: new #2 // class java/util/ArrayList3: dup4: invokespecial #3 // Method java/util/ArrayList.
init:()V7: astore_18: aload_19: bipush 1011: invokestatic #4 // Method
java/lang/Integer.valueOf:(I)Ljava/lang/Integer;14: invokeinterface #5, 2 // InterfaceMethod
java/util/List.add:(Ljava/lang/Object;)Z19: pop20: aload_121: iconst_022: invokeinterface #6, 2 // InterfaceMethod
java/util/List.get:(I)Ljava/lang/Object;27: checkcast #7 // class java/lang/Integer30: astore_231: returnLineNumberTable:line 8: 0line 9: 8line 10: 20line 11: 31LocalVariableTable:Start Length Slot Name Signature0 32 0 args [Ljava/lang/String;8 24 1 list Ljava/util/List;LocalVariableTypeTable:Start Length Slot Name Signature8 24 1 list Ljava/util/ListLjava/lang/Integer;;使用反射仍然能够获得这些信息
public SetInteger test(ListString list, MapInteger, Object map) {
}
Method test Candy3.class.getMethod(test, List.class, Map.class);
Type[] types test.getGenericParameterTypes();
for (Type type : types) {if (type instanceof ParameterizedType) {ParameterizedType parameterizedType (ParameterizedType) type;System.out.println(原始类型 - parameterizedType.getRawType());Type[] arguments parameterizedType.getActualTypeArguments();for (int i 0; i arguments.length; i) {System.out.printf(泛型参数[%d] - %s\n, i, arguments[i]);}}
}输出
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object3.4 可变参数
可变参数也是 JDK 5 开始加入的新特性
Java代码
public class Candy4 {public static void foo(String... args) {String[] array args; // 直接赋值System.out.println(array);}public static void main(String[] args) {foo(hello, world);}
}被编译器转换后的代码
可变参数 String... args 其实是一个 String[] args 从下面的代码中就可以看出来
public class Candy4 {public static void foo(String[] args) {String[] array args; // 直接赋值System.out.println(array);}public static void main(String[] args) {foo(new String[]{hello, world});}
}注意如果调用了 foo() 则等价代码为 foo(new String[]{}) 创建了一个空的数组而不会传递 null 进去 3.5 foreach 循环
仍是 JDK 5 开始引入的语法糖
1.数组的 foreach 循环
Java代码
public class Candy5_1 {public static void main(String[] args) {int[] array {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦for (int e : array) {System.out.println(e);}}
}被编译器转换后的代码
public class Candy5_1 {public Candy5_1() {}public static void main(String[] args) {int[] array new int[]{1, 2, 3, 4, 5};for(int i 0; i array.length; i) {int e array[i];System.out.println(e);}}
}2.集合的 foreach 循环
Java代码
public class Candy5_2 {public static void main(String[] args) {ListInteger list Arrays.asList(1,2,3,4,5);for (Integer i : list) {System.out.println(i);}}
}被编译器转换后的代码
实际被编译器转换为对迭代器的调用
public class Candy5_2 {public Candy5_2() {}public static void main(String[] args) {ListInteger list Arrays.asList(1, 2, 3, 4, 5);Iterator iter list.iterator();while(iter.hasNext()) {Integer e (Integer)iter.next();System.out.println(e);}}
}注意foreach 循环写法能够配合数组以及所有实现了 Iterable 接口的集合类一起使用其中 Iterable 用来获取集合的迭代器 Iterator 3.6 switch
字符串从 JDK 7 开始switch 可以作用于字符串和枚举类这个功能其实也是语法糖
1. switch 字符串
Java代码
public class Candy6_1 {public static void choose(String str) {switch (str) {case hello: {System.out.println(h);break;}case world: {System.out.println(w);break;}}}
}
注意 switch 配合 String 和枚举使用时变量不能为null原因分析完语法糖转换后的代码应当自然清楚
被编译器转换后的代码
public class Candy6_1 {public Candy6_1() {}public static void choose(String str) {byte x -1;switch(str.hashCode()) {case 99162322: // hello 的 hashCodeif (str.equals(hello)) {x 0;}break;case 113318802: // world 的 hashCodeif (str.equals(world)) {x 1;}}switch(x) {case 0:System.out.println(h);break;case 1:System.out.println(w);}}
}
可以看到执行了两遍 switch第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型第二遍才是利用 byte 执行进行比较。
为什么第一遍时必须既比较 hashCode又利用 equals 比较呢hashCode 是为了提高效率减少可能的比较而 equals 是为了防止 hashCode 冲突例如 BM 和 C这两个字符串的hashCode值都是 2123 如果有如下Java代码
public class Candy6_2 {public static void choose(String str) {switch (str) {case BM: {System.out.println(h);break;}case C.: {System.out.println(w);break;}}}
}被编译器转换后的代码
public class Candy6_2 {public Candy6_2() {}public static void choose(String str) {byte x -1;switch(str.hashCode()) {case 2123: // hashCode 值可能相同需要进一步用 equals 比较if (str.equals(C.)) {x 1;} else if (str.equals(BM)) {x 0;}default:switch(x) {case 0:System.out.println(h);break;case 1:System.out.println(w);}}}
}2.switch 枚举
Java代码
enum Sex {MALE, FEMALE
}
public class Candy7 {public static void foo(Sex sex) {switch (sex) {case MALE:System.out.println(男); break;case FEMALE:System.out.println(女); break;}}
}
被编译器转换后的代码
public class Candy7 {/*** 定义一个合成类仅 jvm 使用对我们不可见* 用来映射枚举的 ordinal 与数组元素的关系* 枚举的 ordinal 表示枚举对象的序号从 0 开始* 即 MALE 的 ordinal()0FEMALE 的 ordinal()1*/static class $MAP {// 数组大小即为枚举元素个数里面存储case用来对比的数字static int[] map new int[2];static {map[Sex.MALE.ordinal()] 1;map[Sex.FEMALE.ordinal()] 2;}}public static void foo(Sex sex) {int x $MAP.map[sex.ordinal()];switch (x) {case 1:System.out.println(男);break;case 2:System.out.println(女);break;}}
}
3.7 枚举类
JDK 7 新增了枚举类以前面的性别枚举为例
Java代码
enum Sex {MALE, FEMALE
}转换后代码
public final class Sex extends EnumSex {public static final Sex MALE;public static final Sex FEMALE;private static final Sex[] $VALUES;static {MALE new Sex(MALE, 0);FEMALE new Sex(FEMALE, 1);$VALUES new Sex[]{MALE, FEMALE};}/*** Sole constructor. Programmers cannot invoke this constructor.* It is for use by code emitted by the compiler in response to* enum type declarations.** param name - The name of this enum constant, which is the identifier* used to declare it.* param ordinal - The ordinal of this enumeration constant (its position* in the enum declaration, where the initial constant isassigned*/private Sex(String name, int ordinal) {super(name, ordinal);}public static Sex[] values() {return $VALUES.clone();}public static Sex valueOf(String name) {return Enum.valueOf(Sex.class, name);}
}3.8 try-with-resources
JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources
语法格式
try(资源变量 创建资源对象){} catch( ) {}
Java代码
其中资源对象需要实现 AutoCloseable 接口例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable 使用 try-with-resources 可以不用写 finally 语句块编译器会帮助生成关闭资源代码
public class Candy9 {public static void main(String[] args) {try(InputStream is new FileInputStream(d:\\1.txt)) {System.out.println(is);} catch (IOException e) {e.printStackTrace();}}
}
转换后代码
public class Candy9 {public Candy9() {}public static void main(String[] args) {try {InputStream is new FileInputStream(d:\\1.txt);Throwable t null;try {System.out.println(is);} catch (Throwable e1) {// t 是我们代码出现的异常t e1;throw e1;} finally {// 判断了资源不为空if (is ! null) {// 如果我们代码有异常if (t ! null) {try {is.close();} catch (Throwable e2) {// 如果 close 出现异常作为被压制异常添加t.addSuppressed(e2);}} else {// 如果我们代码没有异常close 出现的异常就是最后 catch 块中的 eis.close();}}}} catch (IOException e) {e.printStackTrace();}}
}为什么要设计一个 addSuppressed(Throwable e) 添加被压制异常的方法呢是为了防止异常信息的丢失想想 try-with-resources 生成的 fianlly 中如果抛出了异常
Java代码
public class Test6 {public static void main(String[] args) {try (MyResource resource new MyResource()) {int i 1/0;} catch (Exception e) {e.printStackTrace();}}
}class MyResource implements AutoCloseable {public void close() throws Exception {throw new Exception(close 异常);}
}
输出
java.lang.ArithmeticException: / by zeroat test.Test6.main(Test6.java:7)Suppressed: java.lang.Exception: close 异常at test.MyResource.close(Test6.java:18)at test.Test6.main(Test6.java:6)
如以上代码所示两个异常信息都不会丢。
3.9 方法重写时的桥接方法
方法重写时对返回值分两种情况
父子类的返回值完全一致子类返回值可以是父类返回值的子类
Java代码
class A {public Number m() {return 1;}
}
class B extends A {Override// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类public Integer m() {return 2;}
}子类转换后的代码
class B extends A {public Integer m() {return 2;}// 此方法才是真正重写了父类 public Number m() 方法public synthetic bridge Number m() {// 调用 public Integer m()return m();}
}
其中桥接方法比较特殊仅对 java 虚拟机可见并且与原来的 public Integer m() 没有命名冲突可以用下面反射代码来验证
for (Method m : B.class.getDeclaredMethods()) {System.out.println(m);
}
会输出
public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()
3.10 匿名内部类
Java代码
public class Candy11 {public static void main(String[] args) {Runnable runnable new Runnable() {Overridepublic void run() {System.out.println(ok);}};}
}
转换后的代码
// 额外生成的类
final class Candy11$1 implements Runnable {Candy11$1() {}public void run() {System.out.println(ok);}
}public class Candy11 {public static void main(String[] args) {Runnable runnable new Candy11$1();}
}
引用局部变量的匿名内部类的Java代码
public class Candy11 {public static void test(final int x) {Runnable runnable new Runnable() {Overridepublic void run() {System.out.println(ok: x);}};}
}
转换后代码
// 额外生成的类
final class Candy11$1 implements Runnable {int val$x;Candy11$1(int x) {this.val$x x;}public void run() {System.out.println(ok: this.val$x);}
}public class Candy11 {public static void test(final int x) {Runnable runnable new Candy11$1(x);}
}
注意
这同时解释了为什么匿名内部类引用局部变量时局部变量必须是 final 的因为在创建 Candy11$1 对象时将 x 的值赋值给了 Candy11$1 对象的 val$x 属性所以 x 不应该再发生变 化了如果变化那么 val$x 属性没有机会再跟着一起变化。
4. 类加载阶段
4.1 加载
将类的字节码载入方法区中内部采用 C 的 instanceKlass 描述 java 类它的重要 field 有
_java_mirror 即 java 的类镜像例如对 String 来说就是 String.class作用是把 klass 暴露给 java 使用_super 即父类_fields 即成员变量_methods 即方法_constants 即常量池_class_loader 即类加载器 _vtable 虚方法表_itable 接口方法表
如果这个类还有父类没有加载先加载父类
加载和链接可能是交替运行的 注意
instanceKlass 这样的【元数据】是存储在方法区1.8 后的元空间内但 _java_mirror 是存储在堆中可以通过前面介绍的 HSDB 工具查看
4.2 链接
链接分为验证、准备、解析三个子阶段
4.2.1 验证
验证类是否符合 JVM 规范安全性检查
用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数检查其修改后是否能够通过安全性检查在控制台运行。
E:\git\jvm\out\production\jvmjava cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again
Exception in thread main java.lang.ClassFormatError: Incompatible magic value
3405691578 in class file cn/itcast/jvm/t5/HelloWorldat java.lang.ClassLoader.defineClass1(Native Method)at java.lang.ClassLoader.defineClass(ClassLoader.java:763)at
java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)at java.net.URLClassLoader.access$100(URLClassLoader.java:73)at java.net.URLClassLoader$1.run(URLClassLoader.java:368)at java.net.URLClassLoader$1.run(URLClassLoader.java:362)at java.security.AccessController.doPrivileged(Native Method)at java.net.URLClassLoader.findClass(URLClassLoader.java:361)at java.lang.ClassLoader.loadClass(ClassLoader.java:424)at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)at java.lang.ClassLoader.loadClass(ClassLoader.java:357)at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
修改完 HelloWorld.class 的魔数后报错信息如上图所示说明验证没有通过
4.2.2 准备
为 static 变量分配空间设置默认值
static 变量在 JDK 7 之前存储于 instanceKlass 末尾从 JDK 7 开始存储于 _java_mirror 末尾static 变量分配空间和赋值是两个步骤分配空间在准备阶段完成赋值在初始化阶段完成如果 static 变量是 final 的基本类型以及字符串常量那么编译阶段值就确定了赋值在准备阶 段完成如果 static 变量是 final 的但属于引用类型那么赋值也会在初始化阶段完成
4.2.3 解析
将常量池中的符号引用解析为直接引用
package cn.itcast.jvm.t3.load;
/**
* 解析的含义
*/
public class Load2 {public static void main(String[] args) throws ClassNotFoundException,
IOException {ClassLoader classloader Load2.class.getClassLoader();// loadClass 方法不会导致类的解析和初始化Class? c classloader.loadClass(cn.itcast.jvm.t3.load.C);new C();System.in.read();}
}class C {D d new D();
}class D {
}
4.3 初始化
1.初始化时调用的方法
初始化即调用cinit()V 方法虚拟机会保证这个类的『构造方法』的线程安全
2.初始化发生的时机
概括得说类初始化是【懒惰的】
main 方法所在的类总会被首先初始化首次访问这个类的静态变量或静态方法时子类初始化如果父类还没初始化会引发子类访问父类的静态变量只会触发父类的初始化Class.forNamenew 会导致初始化
不会导致类初始化的情况
访问类的 static final 静态常量基本类型和字符串不会触发初始化类对象.class 不会触发初始化创建该类的数组不会触发初始化类加载器的 loadClass 方法Class.forName 的参数 2 为 false 时
3.实验验证初始化发生的时机
实验用到的类
class A {static int a 0;static {System.out.println(a init);}
}class B extends A {final static double b 5.0;static boolean c false;static {System.out.println(b init);}
}验证实验时请先全部注释每次只执行其中一个
public class Load3 {static {System.out.println(main init);}public static void main(String[] args) throws ClassNotFoundException {// 1. 静态常量基本类型和字符串不会触发初始化System.out.println(B.b);// 2. 类对象.class 不会触发初始化System.out.println(B.class);// 3. 创建该类的数组不会触发初始化System.out.println(new B[0]);// 4. 不会初始化类 B但会加载 B、AClassLoader cl Thread.currentThread().getContextClassLoader();cl.loadClass(cn.itcast.jvm.t3.B);// 5. 不会初始化类 B但会加载 B、AClassLoader c2 Thread.currentThread().getContextClassLoader();Class.forName(cn.itcast.jvm.t3.B, false, c2);// 1. 首次访问这个类的静态变量或静态方法时System.out.println(A.a);// 2. 子类初始化如果父类还没初始化会引发System.out.println(B.c);// 3. 子类访问父类静态变量只触发父类初始化System.out.println(B.a);// 4. 会初始化类 B并先初始化类 AClass.forName(cn.itcast.jvm.t3.B);}
}5. 类加载器
以 JDK 8 为例 5.1 启动类加载器
用 Bootstrap 类加载器加载类
Java类
package cn.itcast.jvm.t3.load;public class F {static {System.out.println(bootstrap F init);}
}执行
package cn.itcast.jvm.t3.load;
public class Load5_1 {public static void main(String[] args) throws ClassNotFoundException {Class? aClass Class.forName(cn.itcast.jvm.t3.load.F);System.out.println(aClass.getClassLoader());}
}
输出
E:\git\jvm\out\production\jvmjava -Xbootclasspath/a:.
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null-Xbootclasspath 表示设置 bootclasspath
其中 /a:. 表示将当前目录追加至 bootclasspath 之后
可以用这个办法替换核心类
java -Xbootclasspath:java -Xbootclasspath/a:java -Xbootclasspath/p:
5.2 扩展类加载器
Java类
package cn.itcast.jvm.t3.load;
public class G {static {System.out.println(classpath G init);}
}
执行
public class Load5_2 {public static void main(String[] args) throws ClassNotFoundException {Class? aClass Class.forName(cn.itcast.jvm.t3.load.G);System.out.println(aClass.getClassLoader());}
}
输出
classpath G init
sun.misc.Launcher$AppClassLoader18b4aac2
写一个同名的类
package cn.itcast.jvm.t3.load;
public class G {static {System.out.println(ext G init);}
}
打个 jar 包
E:\git\jvm\out\production\jvmjar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 481) (输出 322)(压缩了 33%)将 jar 包拷贝到 JAVA_HOME/jre/lib/ext
重新执行 Load5_2
输出
ext G init
sun.misc.Launcher$ExtClassLoader29453f445.3 双亲委派模式
所谓的双亲委派就是指调用类加载器的 loadClass 方法时查找类的规则
protected Class? loadClass(String name, boolean resolve)throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 1. 检查该类是否已经加载Class? c findLoadedClass(name);if (c null) {long t0 System.nanoTime();try {if (parent ! null) {// 2. 有上级的话委派上级 loadClassc parent.loadClass(name, false);} else {// 3. 如果没有上级了ExtClassLoader则委派BootstrapClassLoaderc findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c null) {long t1 System.nanoTime();// 4. 每一层找不到调用 findClass 方法每个类加载器自己扩展来加载c findClass(name);// 5. 记录耗时sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}例如
public class Load5_3 {public static void main(String[] args) throws ClassNotFoundException {Class? aClass Load5_3.class.getClassLoader().loadClass(cn.itcast.jvm.t3.load.H);System.out.println(aClass.getClassLoader());}
}
执行流程为
1. sun.misc.Launcher$AppClassLoader //1 处 开始查看已加载的类结果没有
2. sun.misc.Launcher$AppClassLoader // 2 处委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()
3. sun.misc.Launcher$ExtClassLoader // 1 处查看已加载的类结果没有
4. sun.misc.Launcher$ExtClassLoader // 3 处没有上级了则委派 BootstrapClassLoader 查找
5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类显然没有
6. sun.misc.Launcher$ExtClassLoader // 4 处调用自己的 findClass 方法是在 JAVA_HOME/jre/lib/ext 下找 H 这个类显然没有回到 sun.misc.Launcher$AppClassLoader 的 // 2 处
7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处调用它自己的 findClass 方法在 classpath 下查找找到了
5.4 线程上下文类加载器
我们在使用 JDBC 时都需要加载 Driver 驱动不知道你注意到没有不写
Class.forName(com.mysql.jdbc.Driver)
也是可以让 com.mysql.jdbc.Driver 正确加载的你知道是怎么做的吗
让我们追踪一下源码
public class DriverManager {// 注册驱动的集合private final static CopyOnWriteArrayListDriverInfo registeredDrivers new CopyOnWriteArrayList();// 初始化驱动static {loadInitialDrivers();println(JDBC DriverManager initialized);}先不看别的看看 DriverManager 的类加载器
System.out.println(DriverManager.class.getClassLoader());打印 null表示它的类加载器是 Bootstrap ClassLoader会到 JAVA_HOME/jre/lib 下搜索类但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包这样问题来了在 DriverManager 的静态代码块中怎么能正确加载 com.mysql.jdbc.Driver 呢
继续看 loadInitialDrivers() 方法
private static void loadInitialDrivers() {String drivers;try {drivers AccessController.doPrivileged(new PrivilegedActionString
() {public String run() {return System.getProperty(jdbc.drivers);}});} catch (Exception ex) {drivers null;}// 1使用 ServiceLoader 机制加载驱动即 SPIAccessController.doPrivileged(new PrivilegedActionVoid() {public Void run() {ServiceLoaderDriver loadedDrivers
ServiceLoader.load(Driver.class);IteratorDriver driversIterator loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});println(DriverManager.initialize: jdbc.drivers drivers);// 2使用 jdbc.drivers 定义的驱动名加载驱动if (drivers null || drivers.equals()) {return;}String[] driversList drivers.split(:);println(number of Drivers: driversList.length);for (String aDriver : driversList) {try {println(DriverManager.Initialize: loading aDriver);// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());} catch (Exception ex) {println(DriverManager.Initialize: load failed: ex);}}
}先看 2发现它最后是使用 Class.forName 完成类的加载和初始化关联的是应用程序类加载器因此 可以顺利完成类加载
再看 1它就是大名鼎鼎的 Service Provider Interface SPI
约定如下在 jar 包的 META-INF/services 包下以接口全限定名名为文件文件内容是实现类名称 这样就可以使用
ServiceLoader接口类型 allImpls ServiceLoader.load(接口类型.class);Iterator接口类型 iter allImpls.iterator();while(iter.hasNext()) {iter.next();}来得到实现类体现的是【面向接口编程解耦】的思想在下面一些框架中都运用了此思想
JDBCServlet 初始化器Spring 容器Dubbo对 SPI 进行了扩展
接着看 ServiceLoader.load 方法
public static S ServiceLoaderS load(ClassS service) {// 获取线程上下文类加载器ClassLoader cl Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}线程上下文类加载器是当前线程使用的类加载器默认就是应用程序类加载器它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载具体代码在 ServiceLoader 的内部类 LazyIterator 中
private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn nextName;nextName null;Class? c null;try {c Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,Provider cn not found);}if (!service.isAssignableFrom(c)) {fail(service,Provider cn not a subtype);}try {S p service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,Provider cn could not be instantiated,x);}throw new Error(); // This cannot happen
}5.5 自定义类加载器
什么时候需要自定义类加载器
1想加载非 classpath 随意路径中的类文件2都是通过接口来使用实现希望解耦时常用在框架设计3这些类希望予以隔离不同应用的同名类都可以加载不冲突常见于 tomcat 容器
步骤
1. 继承 ClassLoader 父类
2. 要遵从双亲委派机制重写 findClass 方法 注意不是重写 loadClass 方法否则不会走双亲委派机制
3. 读取类文件的字节码
4. 调用父类的 defineClass 方法来加载类
5. 使用者调用该类加载器的 loadClass 方法
6.运行期优化
6.1 即时编译