基础不牢,地动山摇

开篇一道题,考察代码执行顺序:

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种情况(参考《深入理解Java虚拟机(第三版)周志明》P359)触发:

  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的各种用法,下一篇就写如何花式编写单例模式。

附上答案:


关注 Java课代表,获取最新 Java 干货

一题搞定static关键字的更多相关文章

  1. 【数据结构】 最小生成树(四)——利用kruskal算法搞定例题×3+变形+一道大水题

    在这一专辑(最小生成树)中的上一期讲到了prim算法,但是prim算法比较难懂,为了避免看不懂,就先用kruskal算法写题吧,下面将会将三道例题,加一道变形,以及一道大水题,水到不用高级数据结构,建 ...

  2. 面试大总结:Java搞定面试中的链表题目总结

    package LinkedListSummary; import java.util.HashMap; import java.util.Stack; /** * http://blog.csdn. ...

  3. 面试大总结之二:Java搞定面试中的二叉树题目

    package BinaryTreeSummary; import java.util.ArrayList; import java.util.Iterator; import java.util.L ...

  4. (转)面试大总结之一:Java搞定面试中的链表题目

    面试大总结之一:Java搞定面试中的链表题目 分类: Algorithm Interview2013-11-16 05:53 11628人阅读 评论(40) 收藏 举报 链表是面试中常出现的一类题目, ...

  5. [转] Java程序员学C#基本语法两个小时搞定(对比学习)

    Java程序员学C#基本语法两个小时搞定(对比学习)   对于学习一门新的语言,关键是学习新语言和以前掌握的语言的区别,但是也不要让以前语言的东西,固定了自己的思维模式,多看一下新的语言的编程思想. ...

  6. Scala入门教程---《chang哥教你一天搞定Scala》

    <chang哥教你一天搞定Scala> /** * <chang哥教你一天搞定Scala> * scala是一门多范式编程语言,集成了面向对象编程和函数式编程等多种特性. * ...

  7. 2017.10.23 Java 面向对象深入学习---final 关键字、static关键字、匿名对象等

    今日内容介绍 1.final 关键字 2.static 关键字 3.匿名对象 4.内部类 5.包的声明与访问 6.访问修饰符 7.代码块 第一节课 01(面向对象)final关键字概念.avi 02: ...

  8. Spring Boot 集成 Ehcache 缓存,三步搞定!

    作者:谭朝红 www.ramostear.com/articles/spring_boot_ehcache.html 本次内容主要介绍基于Ehcache 3.0来快速实现Spring Boot应用程序 ...

  9. 10分钟搞定 Java 并发队列好吗?好的

    | 好看请赞,养成习惯 你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it ...

随机推荐

  1. Vue3 新特性

    一.vue3 为什么要重写 两个主要原因考虑重写vue新版本主要功能: 1.主流浏览器对新的JavaScript语言特性的普遍支持. 2.当前Vue代码库随着时间的推移而暴露出来的设计和体系架构问题. ...

  2. 关键时刻,让你的iphone拒绝掉的所有来电

    夜间被骚扰电话吵醒是会非常烦躁的,以下就是iphone的勿扰模式,配合刚出的夜间深夜模式非常的nice. 可以自定义设置时间段,每天智能切换. 也可以开启个人收藏的白名单,让家人有紧急事情也可以联系到 ...

  3. ArchLinux的安装

    ArichLinux安装教程 Arch Linux 于 2002 年发布,由 Aaron Grifin 领头,是当下最热门的 Linux 发行版之一.从设计上说,Arch Linux 试图给用户提供简 ...

  4. Shell总结01-shell解释器

    常见Shell解释器种类 就像不同地区有不同方言一样,不同的Linux/Unix系统使用着不同类型的shell,其中sh是UNIX上的最基本的shell,遵循POSIX接口规范 操作系统 默认shel ...

  5. ZWave对COMAND CLASS的处理流程

    文章主题  在开发一个 ZWave Device 的过程中,对 COMAND CLASS(单词太长了,后面就简写为 CC 啦) 的处理是最基本.最重要的工作.这篇文章以最最简单的 CC:COMMNAD ...

  6. rust 神奇的特质

    pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> Strin ...

  7. 使用addEventListener绑定事件是关于this和event记录

    DOM元素使用addEventListener绑定事件的时候经常会碰到想把当前作用域传到函数内部,可以使用以下两种放下: var bindAsEventListener=function (objec ...

  8. 一对多分页的SQL到底应该怎么写?

    1. 前言 MySQL一对多的数据分页是非常常见的需求,比如我们要查询商品和商品的图片信息.但是很多人会在这里遇到分页的误区,得到不正确的结果.今天就来分析并解决这个问题. 2. 问题分析 我们先创建 ...

  9. 黎活明8天快速掌握android视频教程--17_创建数据库与完成数据添删改查

    1.我们首先来看下整个项目 项目也是采用mvc的框架 package dB; import android.content.Context; import android.database.sqlit ...

  10. 入门大数据---通过Flume、Sqoop分析日志

    一.Flume安装 参考:Flume 简介及基本使用 二.Sqoop安装 参考:Sqoop简介与安装 三.Flume和Sqoop结合使用案例 日志分析系统整体架构图: 3.1配置nginx环境 请参考 ...