乐趣区

关于java:一题搞定static关键字

根底不牢,地动山摇。大家好,我是课代表。

公众号关注:Java 课代表,获取更多实战干货。

开篇一道题,考查代码执行程序:

public class Parent {
    static {System.out.println("Parent static initial block");
    }

    {System.out.println("Parent initial block");
    }

    public Parent() {System.out.println("Parent constructor block");

    }
}

public class Child extends Parent {
    static {System.out.println("Child static initial block");
    }

    {System.out.println("Child initial block");
    }
    
    private Hobby hobby = new Hobby();

    public Child() {System.out.println("Child constructor block");
    }
}

public class Hobby {
    static{System.out.println("Hobby static initial block");
    }

    public Hobby() {System.out.println("hobby constructor block");
    }
}

当执行 new Child() 时,上述代码输入什么?

置信有不少同学遇到过这类问题,可能查过材料之后接着就忘了,再次遇到还是答不对。接下来课代表通过 4 个步骤,带大家拆解一下这段代码的执行程序,并借此总结法则。

1. 编译器优化了啥?

上面两段代码比照一下编译前后的变动:

编译前的Child.java

public class Child extends Parent {
    static {System.out.println("Child static initial block");
    }
    {System.out.println("Child initial block");
    }
    
    private Hobby hobby = new Hobby();
    
    public Child() {System.out.println("Child constructor block");
    }
}

编译后的Child.class

public class Child extends Parent {
    private Hobby hobby;

    public Child() {System.out.println("Child initial block");
        this.hobby = new Hobby();
        System.out.println("Child constructor block");
    }

    static {System.out.println("Child static initial block");
    }
}

通过比照能够看到,编译器把初始化块和实例字段的赋值操作,挪动到了构造函数代码之前,并且保留了相干代码的先后顺序。事实上,如果构造函数有多个,初始化代码也会被复制多份挪动过来。

据此能够得出第一条优先级程序:

  • 初始化代码 > 构造函数代码

2.static 有啥作用?

类的加载过程可粗略分为三个阶段:加载 -> 链接 -> 初始化

初始化阶段可被 8 种状况周志明》P359 “ 触发类初始化的 8 种状况 ”)触发:

  1. 应用 new 关键字实例化对象的时候
  2. 读取或设置一个类型的动态字段(常量 ”)除外)
  3. 调用一个类型的静态方法
  4. 应用反射调用类的时候
  5. 当初始化类的时候,如果发现父类还没有进行过初始化,则先触发其父类初始化
  6. 虚拟机启动时,会先初始化主类(蕴含 main() 办法的那个类)
  7. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的办法所在的类。
  8. 如果接口中定义了默认办法(default 润饰的接口办法),该接口的实现类产生了初始化,则该接口要在其之前被初始化

其中的 2,3 条目是被 static 代码触发的。

其实初始化阶段就是执行类结构器 <clinit> 办法的过程,这个办法是编译器主动生成的,外面收集了static 润饰的所有类变量的赋值动作和动态语句块(static{} 块),并且保留这些代码呈现的先后顺序。

依据条目 5,JVM 会保障在子类的 <clinit> 办法执行前,父类的 <clinit> 办法曾经执行结束。

小结一下:拜访类变量或静态方法,会触发类的初始化,而类的初始化就是执行 <clinit>,也就是执行 static 润饰的赋值动作和static{} 块,并且 JVM 保障先执行父类初始化,再执行子类初始化。

由此得出第二条优先级程序:

  • 父类的 static 代码 > 子类的 static 代码

3.static 代码只执行一次

咱们都晓得,static代码 (静态方法除外) 只执行一次。

你有没有想过,这个机制是如何保障的呢?

答案是:双亲委派模型。

JDK8 及之前的双亲委派模型是:

应用程序类加载器 → 扩大类加载器 → 启动类加载器

平时开发中写的类,默认都是由 应用程序类加载器加载,它会委派给其父类:扩大类加载器。而扩大类加载器又会委派给其父类:启动类加载器。只有当父类加载器反馈无奈实现这个加载申请时,子加载器才会尝试本人去实现加载,这个过程就是双亲委派。三者的父子关系并不是通过继承,而是通过组合模式实现的。

该过程的实现也很简略,上面展现要害实现代码:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    // 首先查看该类是否被加载过
    // 如果加载过,间接返回该类
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {if (parent != null) {c = parent.loadClass(name, false);
            } else {c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父类抛出 ClassNotFoundException
            // 阐明父类无奈实现加载申请
        }

        if (c == null) {
            // 如果父类无奈加载,转由子类加载
            c = findClass(name);
        }
    }
    if (resolve) {resolveClass(c);
    }
    return c;
}

联合正文置信大家很容易看懂。

由双亲委派的代码可知,同一个类加载器下,一个类只能被加载一次,也就限定了它只能被初始化一次。所以类中的 static代码 (静态方法除外) 只在类初始化时执行一次

4. <init><clinit>

后面曾经介绍了编译器主动生成的类结构器:<clinit>办法,它会收集 static 润饰的所有类变量的赋值动作和动态语句块(static{} 块)并保留代码的呈现程序,它会在类初始化时执行

相应的,编译器还会生成一个 <init> 办法,它会收集实例字段的赋值动作、初始化语句块 ({} 块)和结构器 (Constructor) 中的代码,并保留代码的呈现程序,它会在 new 指令之后接着执行

所以,当咱们 new 一个类时,如果 JVM 未加载该类,则先对其进行初始化,再进行实例化。

至此,第三条优先级规定也就跃然纸上了:

  • 动态代码 (static{} 块、动态字段赋值语句) > 初始化代码 ({} 块、实例字段赋值语句)

5. 法则实际

将前文的三条规定合并,总结出如下两条:

1. 动态代码 (static{} 块、动态字段赋值语句) > 初始化代码 ({} 块、实例字段赋值语句) > 构造函数代码

2. 父类的 static 代码 > 子类的 static 代码

依据前文总结,初始化代码和构造函数代码被编译器收集到了 <init> 中,动态代码被收集到了 <clinit> 中,所以再次对上述法则做合并:

父类<clinit> > 子类<clinit> > 父类 <init> > 子类 <init>

对应到开篇的问题,咱们来实际一下:

当执行 new Child() 时,new 关键字触发了 Child 类的初始化,JVM 发现其有父类,则先初始化 Parent 类,开始执行 Parent 类的 <clinit> 办法,而后执行 Child 类的 <clinit> 办法 (还记得<clinit> 外面收集了什么吗?)。

而后开始实例化 一个 Child 类的对象,此时筹备执行 Child 的 <init> 办法,发现它有父类,优先执行父类的 <init> 办法,而后再执行子类的 <init>(还记得<init> 外面收集了什么吗?)。

置信看到这里,各位心中曾经对开篇的问题有答案了,无妨先手写一下输入程序,而后写代码亲自验证一下。

结束语

平时开发中常常用到static,每次写的时候,心里总会打两个问号,我为什么要用static? 不必行不行?这正应了开篇的第一句话:

根底不牢,地动山摇

通过本文能够看出,static的利用远远不止类变量,静态方法那么简略。在经典的单例模式中,你将看到 static 的各种用法,下一篇就写如何 花式 编写单例模式。



【举荐浏览】
RabbitMQ 教程
Freemarker 教程 (一)- 模板开发手册
下载的附件名总乱码?你该去读一下 RFC 文档了!
深入浅出 MySQL 优先队列(你肯定会踩到的 order by limit 问题)


码字不易,欢送点赞关注和分享。
搜寻:【Java 课代表】,关注公众号,及时获取更多 Java 干货。

退出移动版