• 再谈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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

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

    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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    重新断点调试,会发现执行顺序是:

    • 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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    方法重写

    最后再来解释一下为什么调用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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    final和static单独使用的场景,无非就是 final表示“不能更改”,static表示“属于类”。

    public final class EnumUtil { // 工具类,没必要继承(当然,这玩意可写可不写)
        
    }
    
    public void method() {
        final long userId = 1L; // 不希望这个值被后面的语句覆盖(也是可写可不写)
        // ...
        
    }
    
    // 如果你有需求,不希望子类覆盖某个方法,要么用private,要么用final,取决于你要不要暴露这个方法
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    另外,static有个比较特别的用法,用来修饰内部类。一般来说,static是无法修饰class的:

    img

    但却可以修饰内部类:

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

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

    img

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

  • 相关阅读:
    如何做好商品的库存管理?哪些指标是衡量库存的指标
    【无标题】
    vue项目 i18n 国际化完整教程
    Linux系统调优介绍
    c#调用摄像头进行二维码扫码
    机车整备场数字孪生 | 图扑智慧铁路
    学会玩游戏,智能究竟从何而来?
    Express+MongoDB服务端开发教程
    数学建模之论文
    Echarts-实现3D柱状图
  • 原文地址:https://blog.csdn.net/smart_an/article/details/134502305