作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
从一道面试题说起
public class FuTest {
public static void main(String[] args) {
// 猜猜打印的内容
Zi zi = new Zi();
}
static class Fu {
int a = 10;
public void printA() {
System.out.println("Fu PrintA:" + a);
}
public Fu() {
printA();
}
}
static class Zi extends Fu {
int a = 20;
@Override
public void printA() {
System.out.println("Zi PrintA:" + a);
}
public Zi() {
printA();
}
}
}
我想大部分人应该会猜错。在我解释之前,希望大家把代码复制到本地,断点调试一下,这很重要。
当我们断点跟踪时,会发现程序运行的大致顺序是:
- 初始化Fu的int a
- 调用Fu的构造方法,执行printA()
- 调用Zi的printA():打印 zi.a = 0(因为zi的a还没初始化,默认0)
- 初始化Zi的int a
- 调用Zi的构造方法,执行printA()
- 调用Zi的printA():打印zi.a = 20
我们知道,子类实例化时会隐式调用父类构造器进行初始化工作,如果把这个过程显式化,就是这样:
Zi的构造器中,加不加super()都会调用父类构造器进行初始化,并且如果显式调用super(),则必须放在第一行。
要想解决上面这个面试题,有两个难点要搞清楚:
- 字段的初始化时机
- 方法重写
字段的初始化时机
为了更全面地认识字段的初始化时机,我们改一下上面的程序:
public class FuTest {
public static void main(String[] args) {
Zi zi = new Zi();
}
static class Fu {
// 新增static变量
static int FU_STATIC_A = 10;
int a = 10;
public void printA() {
System.out.println("Fu PrintA:" + a);
}
public Fu() {
printA();
}
}
static class Zi extends Fu {
// 新增static变量
static int ZI_STATIC_A = 20;
int a = 20;
@Override
public void printA() {
System.out.println("Zi PrintA:" + a);
}
public Zi() {
// 为了方便观察,显式调用super()
super();
printA();
}
}
}
重新断点调试,会发现执行顺序是:
-
main方法执行 Zi zi = new Zi()
-
初始化FU_STATIC_A
-
初始化ZI_STATIC_A
-
执行Zi构造器
-
- 初始化Fu
-
-
- 初始化Fu的int a
- 调用Fu的构造器
- 调用printA(),实际调用Zi的printA()
-
-
- 初始化Zi的int a
- 调用Zi的构造器
- 调用printA(),实际调用Zi的printA()
总得来说,分为几个阶段:
-
类加载
-
- 先加载父类
-
-
- 初始化static修饰的字段
-
-
- 后加载子类
-
-
- 初始化static修饰的字段
-
-
对象初始化
-
- 先初始化父“对象”
-
-
- 初始化父“对象”普通字段
- 调用父“对象”构造器
-
-
- 再初始化子对象
-
-
- 初始化子对象普通字段
- 调用子对象构造器
-
类加载阶段所做的事情,大家在学习JVM时都接触过:
类加载的最后阶段,会进行初始化,也就是static相关的一切操作(因为static的操作都是伴随着类加载进行,所以我们说 static是属于类的)。
public class FuTest {
public static void main(String[] args) {
// 0:发现要new Zi,而此时内存中没有Zi这个类,而Zi又继承了Fu,所以会先加载 Fu、再加载 Zi(注意,此时只是类加载!)
// 5:【类加载并初始化】完毕,开始【对象创建和初始化】
Zi zi = new Zi();
}
static class Fu {
// 类加载1:加载Fu,给Fu的静态字段默认初始化
static int FU_STATIC_A = 10;
static {
// 类加载2:调用static代码块,给Fu静态字段初始化
FU_STATIC_A = 11;
}
// 对象初始化7:初始化fu普通字段
int a = 10;
public void printA() {
System.out.println("Fu PrintA:" + a);
}
public Fu() {
// 对象初始化8:调用fu构造器
printA();
}
}
static class Zi extends Fu {
// 类加载3:加载Zi,给Zi的静态字段默认初始化
static int ZI_STATIC_A = 20;
static {
// 类加载4:调用static代码块,给Zi静态字段初始化
ZI_STATIC_A = 21;
}
// 对象初始化9:初始化zi普通字段
int a = 20;
@Override
public void printA() {
System.out.println("Zi PrintA:" + a);
}
public Zi() {
// 对象初始化6:优先初始化父对象
super();
// 对象初始化9:zi构造器执行完毕
printA();
}
}
}
方法重写
最后再来解释一下为什么调用Fu构造器时,最终调用的是Zi的printA(),而不是Fu的printA()。其实就是上一篇讲到的 虚方法表。因为Zi重写了Fu的printA(),那么通过Zi类实例invoke方法时,就会直接调用Zi类重写的方法。而方法打印的字段,一定是调用者this所在的字段(方法执行时,会根据this找到目标对象并处理)!
final的作用
final的作用主要3个:
- final class,不允许extends
- final method,不允许override
- final field,不允许change
其实final本质上就做一件事:把任何动态的统统变成静态的,把不确定的变成确定的。以final method为例,当一个方法被final修饰,那么子类就不允许重写了,所以obj.method()调用时就是确定的。
比如Person也可以调用wait(),但此时查虚方法表只能查到Object原始的wait(),最终是往Object的wait()去了。
final和static实战
实际开发中,final和static组合使用的场景居多:
class XxxService {
// 当我们需要一个 静态常量 时,可以这样写
private static final int a = 1;
// 省略...
}
public final class ConnectionUtils {
private ConnectionUtils() {}
// 全局只要一个tl对象,而且final不允许改变
private static final ThreadLocal<Connection> tl = new ThreadLocal<>();
private static final BasicDataSource DATA_SOURCE = new BasicDataSource();
// 对于static final修饰的DATA_SOURCE,希望做一些较为复杂的赋值工作,可以挪到静态代码块
static {
DATA_SOURCE.setDriverClassName("com.mysql.jdbc.Driver");
DATA_SOURCE.setUrl("jdbc:mysql://localhost:3306/demo");
DATA_SOURCE.setUsername("root");
DATA_SOURCE.setPassword("123456");
}
}
final和static单独使用的场景,无非就是 final表示“不能更改”,static表示“属于类”。
public final class EnumUtil { // 工具类,没必要继承(当然,这玩意可写可不写)
}
public void method() {
final long userId = 1L; // 不希望这个值被后面的语句覆盖(也是可写可不写)
// ...
}
// 如果你有需求,不希望子类覆盖某个方法,要么用private,要么用final,取决于你要不要暴露这个方法
另外,static有个比较特别的用法,用来修饰内部类。一般来说,static是无法修饰class的:
但却可以修饰内部类:
public class UserDTO {
private String name;
private Department department;
// 比如对于一个Response的TO,内部有个字段需要一个TO表示,且只会在你这个接口里使用,就没必要定义为公共类
static class Department {
private String name;
}
}
静态内部类的好处是,外部调用者在new的时候无需实例化外部类:
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬