前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【小家java】类中静态代码块、构造代码块、静态变量、成员变量执行顺序和继承逻辑

【小家java】类中静态代码块、构造代码块、静态变量、成员变量执行顺序和继承逻辑

作者头像
YourBatman
发布2019-09-03 13:46:47
1.4K0
发布2019-09-03 13:46:47
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

1、概述

诚如各位所知,java的三大特性:封装、继承、多态。其中继承,是java中最有学问的一点也是最相对来说最难理解的一些东西,本文针对于此,做一些实例分析,希望能够帮助大家理解java中的继承机制

2、栗子

情况一:当父类和子类有同名同类型的属性时,使用时需要注意

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) {
        Parent chidParent = new Child();
        System.out.println("Parent:" + chidParent.getAge()); //40
        System.out.println("Parent:" + chidParent.age); //18 这个结果你能接受吗?哈哈
        Child child = Child.class.cast(chidParent);
        System.out.println("Child:" + child.getAge()); //40
        System.out.println("Child:" + child.age); //40
    }
}

@Getter
@Setter
class Child extends Parent {
    Integer age = 40; //名称和父类的同名
}

@Getter
@Setter
class Parent {
    Integer age = 18;
}

我们发现,那个18为什么会输出出来呢?父类和子类的变量是同时存在的,即使是同名。子类中看到的是子类的变量,父类中看到的是父类中的变量,它们互相隐藏,而同名的方法则是实实在在的覆盖(重写),属性不存在重写哟。有了这个解释,就好理解了吧 情况二:当父类和子类有同名***不同类型***的属性时,使用时需要注意

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) {
        // 报错Error:(20, 12) java: com.sayabc.boot2demo1.main.Child中的getAge()
        // 无法覆盖com.sayabc.boot2demo1.main.Parent中的getAge()
        // 返回类型java.lang.String与java.lang.Integer不兼容
        Parent chidParent = new Child();
    }
}

@Getter
class Child extends Parent {
    String age = "40"; //名称和父类的同名

    public void setAge(String age) {
        this.age = age;
    }
}

@Getter
class Parent {
    Integer age = 18;

    public void setAge(Integer age) {
        this.age = age;
    }
}

我们高兴的发现,如果类型不同,编译器还发现不了,但是一运行,就报错啦。这算编译器的bug吗?哈哈 情况三:继承中最基本的类加载顺序,不做过多解释。静态代码块只执行一次,并且优先于mai方法先执行

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) {
        Parent chidParent = new Child();
    }
}

@Getter
class Child extends Parent {

    static {
        System.out.println("Child的静态块");
    }

    {
        System.out.println("Child的构造块");
    }

    Child() {
        System.out.println("Child的构造方法");
    }
}

@Getter
class Parent {
    Integer age = 18;

    static {
        System.out.println("Parent的静态块");
    }

    {
        System.out.println("Parent的构造块");
    }

    Parent() {
        System.out.println("Parent的构造方法");
    }
}

结果如下:

代码语言:javascript
复制
Parent的静态块
Child的静态块
Parent的构造块
Parent的构造方法
Child的构造块
Child的构造方法

备注:此处需要注意,此处子类没有显示调用super(),但父类的构造还是执行了的。但是,但是,但是,如果构造快为有参构造,请记得显示调用super方法,否则父类是不能被初始化的。如果子类的构造器没有显示地调用超类的构造器,则将自动调用超类默认(没有参数) 的构造器。如果超类没有不带参数的构造器,并且在子类的构造器又没有显式地调用超类的其他构造器,则 java 编译器将报告错误

情况四:子类和父类有同名同类型的静态常量的时候

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) {
        Parent parent = new Child();
        System.out.println(parent.name); //fangshixiangParent
        Child child = new Child();
        System.out.println(child.name); //fangshixiangChild
    }
}

@Getter
@Setter
class Child extends Parent {
    static String name = "fangshixiangChild";
}

@Getter
@Setter
class Parent {
    static String name = "fangshixiangParent";
}

有了前面的基础,这个现象就很好解释了。同理:当有同名不同类型的属性时,直接获取属性还是会各自获取到自己的,但get方法就不行,就会报错了。 情况五:静态代码块属于类的,并且优先于main方法执行

代码语言:javascript
复制
public class StaticDemo1 {                             

    public static void main(String[] args) {
        StaticDemo1 t1=new StaticDemo1();    //第2步,初始化构造函数,i=9
        System.out.println(t1.i);            //第3步,按顺序执行,9
        speak();                            //第4步,按顺序执行,调用静态函数
        
    }    
    static int i=1;                            //静态变量存到静态区域。    
    static void speak()                        //静态函数存到静态区域。调用时执行。
    {
        System.out.println("a");;
    }
    static {                                //第1步,静态代码块随着类的加载,优先执行且只执行一次。i=3,i+3打印结果是4。
        i=i+3;
        System.out.println(i);
    }
    
    public StaticDemo1(){                    //构造方法,初始化时执行。
        i=i+5;                                //i=9
        System.out.println(i);    
    }
}

但是,但是,但是。如果StaticDemo1没有new或者静态方法没有调用,静态代码块是不会被执行的哦,只有加载了才会执行,并且只执行一次 static块真正的执行时机。如果了解JVM原理,我们知道,一个类的运行分为以下步骤:static代码块真正执行时机 下面我们看看执行static块的几种情况: 1、第一次new A()的过程会打印"";因为这个过程包括了初始化 2、第一次Class.forName(“A”)的过程会打印"";因为这个过程相当于Class.forName(“A”,true,this.getClass().getClassLoader()); 3、第一次Class.forName(“A”,false,this.getClass().getClassLoader())的过程则不会打印""。因为false指明了装载类的过程中,不进行初始化。不初始化则不会执行static块。

最后,附上两张图,大家可以明显发发现一些端倪,希望对大家能有记忆作用哈:

图一:构造代码块是在初始化对象属性(成员变量)之前执行的

这里写图片描述
这里写图片描述

图二:@PostConstruct是对象的属性都初始化ok了才去执行的。

这里写图片描述
这里写图片描述

特别的,这里我介绍一下各种注解影响的执行顺序,如下代码:

代码语言:javascript
复制
@Component  
public class InitBeanTest implements InitializingBean,ApplicationListener<ContextRefreshedEvent> {  
  
    @Resource  
    DemoService demoService;  
      
    public InitBeanTest() {     
           System.err.println("----> InitSequenceBean: constructor: "+demoService);     
        }  
  
    @PostConstruct  
    public void postConstruct() {  
        System.err.println("----> InitSequenceBean: postConstruct: "+demoService);  
    }  
  
    @Override  
    public void afterPropertiesSet() throws Exception {  
        System.err.println("----> InitSequenceBean: afterPropertiesSet: "+demoService);  
    }  
  
    @Override  
    public void onApplicationEvent(ContextRefreshedEvent arg0) {  
        System.err.println("----> InitSequenceBean: onApplicationEvent");  
    }  
  
}  

执行结果:

----> InitSequenceBean: constructor: null
----> InitSequenceBean: postConstruct: com.yiniu.kdp.service.impl.DemoServiceImpl@40fe544
----> InitSequenceBean: afterPropertiesSet: com.yiniu.kdp.service.impl.DemoServiceImpl@40fe544
----> InitSequenceBean: onApplicationEvent
----> InitSequenceBean: onApplicationEvent

根据执行结果,我们很容易的总结出来执行的顺序。至于是什么原因呢,下面给出一个简单分析:

构造函数是每个类最先执行的,这个时候,bean属性还没有被注入。 postConstruct优先于afterPropertiesSet执行,这时属性竟然也被注入了,这个时候需要记住啦 spring很多组建的初始化都放在afterPropertiesSet做。我们在做一些中间件想和spring一起启动,可以放在这里启动。比如获取到ApplicationContext上下文 onApplicationEvent属于应用层的事件,最后被执行,很容易理解。注意,它出现了两次,为什么?因为bean注入了DemoService,spring容器会被刷新。这个很容易被当作高级面试题的哟。换言之onApplicationEvent会被频繁执行,需要使用它监听,需要考虑性能问题以及重复执行的问题。很显然,这是观察者模式的经典应用。至于spring中观察者模式的使用,我在后续文章中会重点分享,请持续关注

3、使用场景

各种设计模式,都会以此为依托,才能有更好的设计

子类默认调用父类构造函数问题

默认情况下,子类在使用构造函数初始化时(不管是子类使用有参构造还是无参构造),默认情况下都会调用父类的无参构造函数(相当于默认情况调用了super())。

下面看看几个变种如下:

1、父类木有空的构造 只有一个有参构造
代码语言:javascript
复制
class Parent{
    private Integer id;
    public Parent(Integer id){
        System.out.println("this is parent cons...");
    }
}

此时候我们发现发现如下三问题: 1、子类Child必须有对应的有参构造

在这里插入图片描述
在这里插入图片描述

2、super(id)必须显示的写出,否则编译不通过

在这里插入图片描述
在这里插入图片描述

3、原则上,子类的构造函数不能多于父类的

在这里插入图片描述
在这里插入图片描述

4、子类构造函数若多余父类(或者类型啥的和父类不匹配),需要显示的调用父类构造函数

在这里插入图片描述
在这里插入图片描述

总结:所有的case总结就这两个原则

  • 1、子类构造器质性之前必须能够先执行父类的构造函数(super(xxx)必须放在第一行代码)
  • 2、若父类有空构造,子类构造默认都会调用super()。若父类木有空构造,子类所有构造都必须显示调用super(xxx)·

4、最后

java的三大特性都非常的重要,如果不理解虚拟机对类的一些处理,有时候会犯迷糊,影响逻辑的设计,所以此文用简单用例希望能帮助大家理解。日后也许会持续更新

附:面试题
代码语言:javascript
复制
public class StaticTest {

    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    StaticTest() {
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {
        System.out.println("4");
    }

    int a = 110;
    static int b = 112;
}

问题:请问这段程序的输出是什么?

可以想象的是,答案五花八门。我觉得脑子里应该浮现出一个这样的知识点:

Java中赋值顺序: 1、父类的静态变量赋值 2、自身的静态变量赋值 3、父类成员变量赋值和父类块赋值 4、父类构造函数赋值 5、自身成员变量赋值和自身块赋值 6、自身构造函数赋值

这个理论咋一看,没有毛病啊。按照这个理论输出是什么呢?答案输出:1 4,这样正确嚒?不卖关子了,下面我给出正确输出为:

代码语言:javascript
复制
2
3
a=110,b=0
1
4

这里不是说上面的规则不正确,而是说不能简单的套用这个规则

这里必须要记住一个结论:实例初始化不一定要在类初始化结束之后才开始初始化

解释:

类的生命周期是:加载->验证->准备->解析->初始化->使用->卸载。

只有在准备阶段和初始化阶段才会涉及类变量的初始化和赋值,因此只针对这两个阶段进行分析;

类的准备阶段:需要做是为类变量(static变量)分配内存并设置默认值(注意此处都是先给默认值),因此类变量st为null、b为0;

需要注意的是,如果类变量是final的,编译时javac就会为它赋上值。因此上面如果我们这样写static final int b=112它哪怕在准备阶段,值就应该是112了

类的初始化阶段:需要做的是执行类构造器请注意:这里不是指的构造函数)。

类构造器:编译器收集所有静态语句块类变量赋值语句,按语句在源码中的顺序合并生成类构造器

因此现在执行:st = new StaticTest().此时我们发现,就会进行对象的初始化了(看到没,这个时候b变量的赋值语句还没有执行哦~~~)

而对象初始化的顺序为:成员变量 -> 普通代码块 -> 构造函数 因此这一波过后:a=110了。 输出为:

代码语言:javascript
复制
2
3
a=110,b=0

需要注意的是,此时b仍然为0,并没有被赋值哦~~~~~

到此st = new StaticTest()这句就执行结束了。继续执行类构造器,显然就会执行static语句块了~~~输出1,最后调用静态方法,就输出4了 完美~

冷知识

可能通过结果看,有点颠覆我们之前的认知。其实这是一个冷知识:

它的关键在于:static StaticTest st = new StaticTest()这句代码,内嵌的这个变量恰好是个静态成员,而且是本类的实例 这就导致了这个有趣的现象:“实例初始化竟然出现在静态初始化之前”。

这里面我只做一小步变化:

代码语言:javascript
复制
static StaticTest st = new StaticTest()
改成
StaticTest st = new StaticTest()
或者改成:
static Object st = new Object();

最终输出结果都为(符合我们常识了吧,啊哈哈哈哈):

代码语言:javascript
复制
1
4
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年05月17日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、概述
  • 2、栗子
  • 3、使用场景
    • 子类默认调用父类构造函数问题
    • 4、最后
      • 附:面试题
      相关产品与服务
      消息队列 TDMQ
      消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档