聊聊Java构造函数的几个“陷阱”

Java属性的实例化、构造函数的执行是有先后顺序的。 此外 当出现子类继承情况时,子类和父类也是有初始化顺序的,这让情况更加复杂! 下面我们重点看几个常见的构造函数陷阱!

对象属性间的平行依赖

属性的实例化和 构造函数执行顺序是有先后的,如果构造函数和属性实例之间存在依赖顺序,请小心!请看下面的例子。

kotlin 复制代码
public class Context{
    private A a;
    private B b;
}

属性 ab我们认为他们是"平行的"。现在他们没有存在互相依赖

css 复制代码
Public class Context{
    Private A a = new A();
    Private B b = new B(a);
}

现在呢,可以认为 b 平行依赖于 a 。这就是对象属性间的平行依赖。当前这种情形是没有问题的。下面的场景有问题!

css 复制代码
Public class Context{
    Private A a;
    Private B b = new B(a); //此时B如果实例化,那么a为null
    Public Context(A a){
        This.a = a;
    }
}

错误出现了,之所以B接受了一个null值,是因为属性 b 的实例化要优先于构造方法的。至于NullPointerException什么时候触发,没人能预测。如果是在B构造方法中触发空指针异常,你可能会恍然大悟,"原来传入了一个null值"。但是如果是在B的常规方法调用触发,你可能需要花点时间来排查空指针异常的原因了。

记住:实例属性的实例代码块要优于构造方法执行。

建议:不要依赖Java的对象初始化顺序,尽量将属性初始化放到构造方法中。

有些人喜欢在声明属性的同时进行初始化,并且还设成了final常量。这样的编码方式让人感到舒适,代码的可读性也相对较高。然而,当代码变得复杂,存在"对象属性的平行依赖"的情况时,就需要小心了!

对于这一点,C++的规范做得很好,属性声明时不能进行初始化,只能在构造函数中进行初始化。所以我们经常看到C++中的构造函数非常冗长,只做了简单的赋值操作。

构造函数陷阱

"构造函数陷阱":构造方法中调用可被重写的方法

javascript 复制代码
Public class A{
    Public A(){
        ....//初始化操作
        function();
    }

如果A的子类重写了function方法,那么A类构造方法执行的就是其子类的function实现,如果A类设计时没有考虑到这种情况,那么A的初始化就存在很大风险。所以要将 function 设为 private,或 final

建议:构造方法内不要使用public方法,如果必须要使用,则注意:子类可能会重写该方法,进而影响父类的初始化过程。

建议将存在子类重写需求的逻辑抽象出一个方法,设置为 protected 或者 public 方法设置为 final。避免子类重写 public 方法。提供给子类覆盖的方法设置为 protected,更加清晰。

另外记住 private 方法中 调用 public 方法也要想到子类可能会重写这个 public 方法~

这就是为什么不推荐使用继承的原因!子类重写父类方法 风险是很高的事情。

下面还会讲解构造方法中 执行 public 方法有多坑。

构造函数与重写带来的空指针异常

scss 复制代码
Public class Context{
    Public Context(){//构造方法
        ....
        Register();
        ....
    }

    Public void register(){
        beforeRegister();
        .....
        afterRegister();
    }
    
    Protected void beforeRegister(){
    }
    
    Protected void afterRegister(){
    }
}

现在 Context 需要向外提供注册功能.但是实例化时,需要先注册一些服务。注册操作前后会 执行 before 方法和 after 方法,子类可以重写,向提供扩展注册功能

当子类重写了 before,after 功能,烦人的空指针异常又出现了。

子类重写了注册的beforeafter方法,为了监听注册功能,在子类属性中维护了额外的数据结构。但是正是子类属性触发了空指针异常。

原因是父类的构造方法中执行了子类重写后的方法,子类重写的方法中使用了自己的属性。因为这个属性还未初始化,所以出现了空指针!

解决思路

最简单的思路是将子类中的变量map声明为static,这就先于父类构造方法执行。这的确能解决这个bug,但引入新的bug。

static变量属于整个类。不单单属于一个对象。那意味着所有的对象实例都共用这个static map, 这不是正确的逻辑。并且static map 在该进行垃圾回收时无法被回收。没准哪个时刻出现了 Out of Memory 内存溢出,服务器宕机,然后查到了这个root 根节点大对象正是static map.导致不可挽回的但本可避免的过失

另一个有争议的实现方法

父类构造方法中 先调用beforeInitialize,同时 beforeInitialize()方法供子类重写,这时子类就可以把属性初始化需求放到 beforeInitialize()方法中。实现了父类对子类的依赖,实现子类属性 先于 父类初始化,但是这么做倒转了依赖,破坏了子类,父类初始化顺序。

此外我们还可以在 beforeRegister 中 判断属性变量是否为 null,如果为 null 就初始化它。这个方法也不是很优雅

还有好多方法...但是归根结底,我们是在构造方法中调用可重写方法倒置了子类和父类的依赖,让父类依赖于子类,这与 Java面向对象的设计理念相冲突,才会出现这么多问题。

建议:尽量不要在构造方法中调用可以可被重写的方法。Public,protected方法尽可能少的出现在构造方法中

"构造函数陷阱":构造方法中调用可被的重写方法

所以为什么有人一直强调,谨慎使用继承,优先使用委托。这就是原因!

父类对子类的依赖

面向对象的设计中,子类可以访问到父类方法,属性,但是父类无法访问子类属性,本应该是子类依赖于父类。

但是 设计模式中模板方法 的思路带来了父类对子类的依赖。(模板方法:父类的方法中调用了方法A,但是方法A由子类具体实现)模板设计虽然由父类定义了逻辑处理的流程,子类只是填空。但是父类的处理逻辑中包括了不可预测的子类实现。

模板方法要求父类充分考虑子类可能的具体实现,考虑哪种实现是正确的,符合要求的。所以模板的设计需要高度的抽象。

之前已经谈到了,构造方法中尽量不要使用模板方法的设计。

总结

通过这几种场景的分析会发现,继承模式有太多潜在的初始化问题、方法重写问题,使用难度远高于委托模式。我建议大家,尽量使用委托模式,而不是继承模式。

相关推荐
新手小袁_J18 分钟前
JDK11下载安装和配置超详细过程
java·spring cloud·jdk·maven·mybatis·jdk11
呆呆小雅19 分钟前
C#关键字volatile
java·redis·c#
Monly2120 分钟前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
Ttang2322 分钟前
Tomcat原理(6)——tomcat完整实现
java·tomcat
goTsHgo23 分钟前
在 Spring Boot 的 MVC 框架中 路径匹配的实现 详解
spring boot·后端·mvc
钱多多_qdd33 分钟前
spring cache源码解析(四)——从@EnableCaching开始来阅读源码
java·spring boot·spring
waicsdn_haha35 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
Q_19284999061 小时前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
Code_流苏1 小时前
VSCode搭建Java开发环境 2024保姆级安装教程(Java环境搭建+VSCode安装+运行测试+背景图设置)
java·ide·vscode·搭建·java开发环境
良许Linux1 小时前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网