再谈super、static、final

作者简介:大家好,我是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();
        }
    }
}

我想大部分人应该会猜错。在我解释之前,希望大家把代码复制到本地,断点调试一下,这很重要。

img

当我们断点跟踪时,会发现程序运行的大致顺序是:

  • 初始化Fu的int a
  • 调用Fu的构造方法,执行printA()
  • 调用Zi的printA():打印 zi.a = 0(因为zi的a还没初始化,默认0)
  • 初始化Zi的int a
  • 调用Zi的构造方法,执行printA()
  • 调用Zi的printA():打印zi.a = 20

我们知道,子类实例化时会隐式调用父类构造器进行初始化工作,如果把这个过程显式化,就是这样:

img

Zi的构造器中,加不加super()都会调用父类构造器进行初始化,并且如果显式调用super(),则必须放在第一行。

img

要想解决上面这个面试题,有两个难点要搞清楚:

  • 字段的初始化时机
  • 方法重写

字段的初始化时机

为了更全面地认识字段的初始化时机,我们改一下上面的程序:

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时都接触过:

img

类加载的最后阶段,会进行初始化,也就是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找到目标对象并处理)!

img

final的作用

final的作用主要3个:

  • final class,不允许extends
  • final method,不允许override
  • final field,不允许change

其实final本质上就做一件事:把任何动态的统统变成静态的,把不确定的变成确定的。以final method为例,当一个方法被final修饰,那么子类就不允许重写了,所以obj.method()调用时就是确定的。

img

比如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的:

img

但却可以修饰内部类:

public class UserDTO {
    
    private String name;
    private Department department;
    
    // 比如对于一个Response的TO,内部有个字段需要一个TO表示,且只会在你这个接口里使用,就没必要定义为公共类
    static class Department {
        private String name;
    }
    
}

静态内部类的好处是,外部调用者在new的时候无需实例化外部类:

img

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602
进群,大家一起学习,一起进步,一起对抗互联网寒冬

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值