01 接口初始化规则
-
接口中只能有常量以及抽象方法,即使没有static final修饰也会默认加上。所以除非是运行期常量,否则该变量直接就在编译阶段放到了调用该常量的方法所在的类的常量池中。
例1:
package com.test02; public class Test01 { public static void main(String[] args) { System.out.println(MyChild5.c); } } interface MyParent5 { int p = 5; } interface MyChild5 extends MyParent5 { int c = 4; }
例2:
interface MyChild5 extends MyParent5 { int c = new Random().nextInt(5); }
删除MyChild5.class文件之后,运行报错:
删除MyParent5.class文件之后,运行报错,因为子类的初始化会导致父类的初始化:
02 类加载准备阶段和初始化阶段代码分析
- 上文介绍了类加载、连接、初始化的几个阶段。我们通过代码来体会一下这几个阶段的意义:
package com.test02;
public class Test02 {
public static void main(String[] args) {
SingleTon s1 = SingleTon.getS();
System.out.println(s1.var1);
System.out.println(s1.var2);
}
}
class SingleTon{
//注:必须都是静态变量,否则无法看到效果
public static int var1;
public static int var2 = 0;
private static SingleTon s = new SingleTon();
private SingleTon() {
var1++;
var2++;
}
public static SingleTon getS() {
return s;
}
}
改变声明成员变量的位置:
class SingleTon{
//注:必须都是静态变量,否则无法看到效果
public static int var1;
private static SingleTon s = new SingleTon();
public static int var2 = 0;
……
- 解释:类在编写完成后,保存,自动编译成了字节码文件。右击run as运行,运行过程中,主函数中调用了静态方法,所以会发生初始化。而初始化之前,会进行:第一步,加载,将字节码文件加载到内存之中;第二步,连接,分为三个阶段。准备阶段会为静态变量赋默认值,分别为0、null、0。到了初始化阶段,var1仍然为默认值为0,new SingleTon()使得两个变量变为了1,最后初始化var2时,又将其变成了0;
03 程序的完整执行流程
举例:
package com.test;
public class Test1 {
static {
System.out.println("main方法初始化");
}
public static void main(String[] args) {
System.out.println(Child.p);
}
}
class Parent{
public static String p = "我是爸爸";
static {
System.out.println("父类初始化");
}
}
class Child extends Parent{
public static String c = "我是儿子";
static {
System.out.println("子类初始化");
}
}
运行截图:
相关图解:
04 类的加载
- 类加载的最终产品是位于内存中的Class对象
- Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。这就是反射的基本原理。Class对象就像一面镜子。
- 两种类型的加载器
- java虚拟机自带的加载器
- 根类加载器(BootStrap):又名启动类加载器
- 扩展类加载器(Extension)
- 系统(应用)类加载器(System)
- 用户自定的类加载器
- java.lang.ClassLoader的子类:位于java.lang包下;
- 用户可定制的加载方式
- java虚拟机自带的加载器
- 类加载器并不需要等到某个类“首次主动使用”时再加载它。
- JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)
解释:我们之前的例子也可以看出,当主动使用父类的静态变量的时候,并不会导致子类初始化。但是它不一定不加载,原因就在于“预料”这两个字,到底是怎么个预料法。上面那张图,就很明显,当主函数发现主动使用了Parent,赶紧加载它,同时捎带也把子类Child加载了,但很明显子类并未初始化,但加载了。 - 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
05 类的验证
- 类被加载后,就进入链接阶段。链接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
- 类验证的内容(不止这些,比如魔数的检查)
- 类文件结构的检查
- 语义检查
- 字节码验证
- 二进制兼容性的验证
06 类的准备
-
在准备阶段,java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于以下 Sample 类,在准备阶段,将为 int 类型的静态变量 a 分配 4 个字节的内存空间,并且赋予默认值 0,为long类型的静态变量 b 分配 8 个字节的内存空间,并且赋予默认值 0。
public class Sample{ private static int a = 1; private static long b; static{ b = 2; } ... }
07 类的初始化
-
几个关键点
-
类的初始化步骤
- 假如这个类没有被加载和连接,那就先进行加载和连接
- 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类
- 假如类中存在初始化语句,那就依次执行这些初始化语句
-
类的初始化时机(7个)
解释如下:package com.test02; import java.util.Random; public class Test03 { public static void main(String[] args) { System.out.println(I2.b); } } interface I1{ public static int a = 1; } interface I2 extends I1{ public static int b = new Random().nextInt(5); }
验证一下确实I2被初始化了:
interface I2 extends I1{
public static int b = new Random().nextInt(5);
public static Thread thread = new Thread() {
{
System.out.println("I2被初始化");
}
};
}
解释一下上述代码,在准备阶段会给thread赋值为null,在初始化阶段会执行new Thead来赋予默认值,从而执行实例化代码块
验证第一条:
interface I1{
public static int a = 1;
public static Thread thread = new Thread() {
{
System.out.println("I1被初始化");
}
};
}
class I2 implements I1{
public static int b = new Random().nextInt(5);
public static Thread thread = new Thread() {
{
System.out.println("I2被初始化");
}
};
}
验证第二条
interface I1{
public static int a = 1;
public static Thread thread = new Thread() {
{
System.out.println("I1被初始化");
}
};
}
interface I2 extends I1{
public static int b = new Random().nextInt(5);
public static Thread thread = new Thread() {
{
System.out.println("I2被初始化");
}
};
}
验证最后结论,在主程序使用是接口变量,如果在编译器能够确定下来是绝不会初始化的。这里的变量指的是需在运行期确定的值:
package com.test02;
import java.util.Random;
public class Test03 {
public static void main(String[] args) {
System.out.println(I2.b);
System.out.println(I1.threadP.hashCode());
}
}
interface I1{
public static int a = 1;
public static Thread threadP = new Thread() {
{
System.out.println("I1被初始化");
}
};
}
interface I2 extends I1{
public static int b = new Random().nextInt(5);
public static Thread thread = new Thread() {
{
System.out.println("I2被初始化");
}
};
}
- 只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或者接口的主动使用
- 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
08 类加载器
-
概念
-
分类
-
关系图
-
在双亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器之外,其余的类加载器有且只有一个父加载器(逻辑意义上)
-
加载过程
当我们自定义的类加载器loader1加载我们写的普通类Sample的时候,因为采用的是双亲委托机制,所以向上抛,交给系统类加载器,而它拿到之后,也向上抛,交给扩展类加载器,扩展类加载器拿到之后,也向上抛,交给根类加载器,根类加载器抛不动了,开始加载这个类,从图一中可知,它的辖区内没这个类。所以向下交给扩展类加载器,它的辖区内也没。所以交个系统类加载器,还记得我们的jdk的环境变量吗?.;C:\Program Files\Java\jdk1.8.0_121\lib,其中“.”表示的是java源文件经过 javac 命令编译并在 .java 文件当前目录生成.class字节码文件。然后用java命令运行字节码文件时会参考classpath中的参数,如果在参数中没有配置“.”也就是“当前目录”(“当前目录” 意思为.java源文件所在的目录)的话java虚拟机会报一个找不到main函数入口的错误。也就是说这时的java找不到刚才好的.class文件在哪里(因为classpath中没有告诉它去哪里找)。可以尝试删掉ClassPath变量,你会发现,用cmd命令行在运行java命令的时候,会报错说无法加载主类,就是因为找不到A.class文件。如果加上这个变量放在不同的文件夹下,也会报错,这就是“.”的作用。而用Eclipse这种半自动化工具是不存在这个问题的,而且它的目录也不再同一文件夹下,应该是eclipse将自动扫描到相应目录下的jdk版本,然后自己会对环境变量做一套处理。因为jdk都找到了,里面的目录又是固定的,我可以做相应的处理来方便项目的构建。
-
上例中的loader1和系统类加载器就是初始类加载器。
-
案例:
package com.test02; public class Test04 { public static void main(String[] args) throws ClassNotFoundException { Class<?> clazz1 = Class.forName("java.lang.String"); System.out.println(clazz1.getClassLoader()); Class<?> clazz2 = Class.forName("com.test02.UserClass"); System.out.println(clazz2.getClassLoader()); } } class UserClass{ }