当前位置: 首页 > article >正文

【潜意识Java】探寻Java子类构造器的神秘面纱与独特魅力,深度学习子类构造器特点

目录

一、子类构造器的诞生背景

(一)为啥要有子类构造器?

(二)子类与父类构造器的关系

二、子类构造器的调用规则

(一)默认调用父类无参构造器

(二)显式调用父类构造器

(三)构造器链的形成

三、子类构造器中的初始化顺序

(一)先初始化父类成员,再初始化子类成员

(二)静态成员的初始化时机

四、子类构造器与方法重写的关系

(一)构造器中调用可重写方法的风险

(二)正确的使用方式

五、子类构造器在实际开发中的应用场景

(一)图形绘制系统中的子类构造器

(二)游戏角色创建中的子类构造器

 

宝子们,今天咱们来深入挖掘 Java 中一个非常关键但又有点让人摸不着头脑的知识点 —— 子类构造器。当我们在 Java 的世界里创建子类对象时,子类构造器就像是一个幕后的魔法工匠,默默地构建出符合我们需求的对象实例。它有着一些独特的特点和规则,掌握好这些,能让我们的代码更加健壮、灵活,并且能够更好地理解 Java 面向对象编程的精髓。接下来,就让我们一步步揭开子类构造器的神秘面纱。

15e685e9f9c84104974d6d585ae74e28.jpg

一、子类构造器的诞生背景

(一)为啥要有子类构造器?

个人观点嘛,你就想象一下,我们有一个基类(父类),它就像是一个汽车的基础模板,定义了汽车的一些基本属性和行为,比如有轮子、引擎,能跑能停。现在,我们想要创建一个子类,比如一辆跑车,它不仅具有汽车的基本特性,还拥有自己独特的属性和行为,比如更快的速度、更酷炫的外观。子类构造器的作用就是在创建这个跑车对象时,既要确保它继承了汽车的基本构造,又能对其独特的属性进行初始化,使它成为一个完整且独特的跑车实例。

当然......

你在实际编程中,父类可能定义了一些通用的属性和方法,子类在继承这些的基础上,通过自己的构造器来扩展和定制,以满足特定的业务需求。例如,在一个图形绘制的系统中,有一个父类 Shape,它定义了颜色、位置等基本属性和绘制的基本方法。子类 Circle 和 Rectangle 除了继承这些基本属性和方法外,还需要通过自己的构造器来初始化半径(对于圆形)和长、宽(对于矩形)等特定属性,这样才能准确地绘制出不同的图形。

157767067a81409db7567c2e939af1b8.jpg

 

(二)子类与父类构造器的关系

子类构造器和父类构造器紧密相连,就像孩子与父母的关系一样。子类构造器在初始化对象时,默认会先调用父类的构造器,确保父类部分的属性和行为得到正确的初始化。这是因为子类是基于父类扩展而来的,只有父类的基础打好了,子类才能在这个基础上进行进一步的构建。

二、子类构造器的调用规则

(一)默认调用父类无参构造器

当我们在子类中没有显式地调用父类的构造器时,Java 编译器会自动在子类构造器的第一行插入对父类无参构造器的调用,就像下面这样:

class Parent {
    public Parent() {
        System.out.println("父类无参构造器被调用");
    }
}

class Child extends Parent {
    public Child() {
        // 这里编译器会自动添加 super(); 来调用父类无参构造器
        System.out.println("子类无参构造器被调用");
    }
}

public class ConstructorExample1 {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

在这个例子中,当创建 Child 类的对象时,首先会输出 “父类无参构造器被调用”,然后才输出 “子类无参构造器被调用”,这就证明了子类构造器默认会先调用父类无参构造器。

是不是很简单呢?包子。

(二)显式调用父类构造器

如果父类没有无参构造器,或者我们想要调用父类的有参构造器来初始化父类的特定属性,就需要在子类构造器中显式地使用 super 关键字来调用父类的构造器,而且 super 调用必须放在子类构造器的第一行,否则会报错。例如:

class ParentWithArgs {
    private int num;

    public ParentWithArgs(int num) {
        this.num = num;
        System.out.println("父类有参构造器被调用,num = " + num);
    }
}

class ChildWithArgs extends ParentWithArgs {
    public ChildWithArgs(int num) {
        // 显式调用父类有参构造器
        super(num);
        System.out.println("子类构造器被调用");
    }
}

public class ConstructorExample2 {
    public static void main(String[] args) {
        ChildWithArgs child = new ChildWithArgs(5);
    }
}

在这个例子中,ChildWithArgs 类的构造器通过 super(num) 显式地调用了父类 ParentWithArgs 的有参构造器,确保父类的 num 属性得到正确的初始化,然后再执行子类构造器中的代码。

(三)构造器链的形成

当存在多层继承关系时,就会形成构造器链。比如说,有一个爷爷类 Grandparent,一个父类 Parent 继承自 Grandparent,一个子类 Child 继承自 Parent。在创建 Child 类的对象时,首先会调用 Grandparent 的构造器,然后是 Parent 的构造器,最后才是 Child 的构造器,就像一条链条一样,一环扣一环,保证了整个继承体系中对象的正确初始化。

class Grandparent {
    public Grandparent() {
        System.out.println("爷爷类构造器被调用");
    }
}

class Parent extends Grandparent {
    public Parent() {
        System.out.println("父类构造器被调用");
    }
}

class Child extends Parent {
    public Child() {
        System.out.println("子类构造器被调用");
    }
}

public class ConstructorChainExample {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

在这个例子中,创建 Child 类对象时,会依次输出 “爷爷类构造器被调用”、“父类构造器被调用” 和 “子类构造器被调用”,清晰地展示了构造器链的执行过程。

87aad87ec1db4c47ba2ad27edde51d76.png

 

三、子类构造器中的初始化顺序

(一)先初始化父类成员,再初始化子类成员

在子类构造器执行过程中,首先会按照父类构造器的初始化逻辑来初始化父类的成员变量,然后才会执行子类构造器中对子类成员变量的初始化操作。这是因为子类对象的内存空间中包含了父类的部分,只有先确保父类部分的完整性,才能正确地初始化子类的扩展部分。比如嘛:

class ParentMember {
    private int parentNum;

    public ParentMember() {
        parentNum = 10;
        System.out.println("父类成员变量初始化,parentNum = " + parentNum);
    }
}

class ChildMember extends ParentMember {
    private int childNum;

    public ChildMember() {
        childNum = 20;
        System.out.println("子类成员变量初始化,childNum = " + childNum);
    }
}

public class InitializationOrderExample {
    public static void main(String[] args) {
        ChildMember child = new ChildMember();
    }
}

在这个例子中,创建 ChildMember 类对象时,先输出 “父类成员变量初始化,parentNum = 10”,然后才输出 “子类成员变量初始化,childNum = 20”,这表明了成员变量的初始化顺序是先父类后子类。

48f3f9a6a3b04570abf6d3cf32fc0c39.png

 

(二)静态成员的初始化时机

静态成员变量和静态代码块的初始化与实例成员的初始化有所不同。静态成员在类加载时就会被初始化,而且只会初始化一次,无论创建多少个该类的对象。对于子类来说,父类的静态成员会先于子类的静态成员初始化,遵循类加载的顺序。例如:

class ParentStatic {
    public static int parentStaticNum;

    static {
        parentStaticNum = 5;
        System.out.println("父类静态成员初始化,parentStaticNum = " + parentStaticNum);
    }
}

class ChildStatic extends ParentStatic {
    public static int childStaticNum;

    static {
        childStaticNum = 8;
        System.out.println("子类静态成员初始化,childStaticNum = " + childStaticNum);
    }
}

public class StaticInitializationExample {
    public static void main(String[] args) {
        // 这里只是为了触发类加载,即使不创建对象,静态成员也会初始化
        System.out.println("父类静态成员:" + ParentStatic.parentStaticNum);
        System.out.println("子类静态成员:" + ChildStatic.childStaticNum);
    }
}

这个例子中,当运行 main 方法时,首先会输出 “父类静态成员初始化,parentStaticNum = 5”,然后输出 “子类静态成员初始化,childStaticNum = 8”,这说明静态成员的初始化是按照类的层次结构,从父类到子类依次进行的,并且与对象的创建无关。

78cb9c8ac7044fa79f0b42165c5ae29d.png

 

四、子类构造器与方法重写的关系

(一)构造器中调用可重写方法的风险

在子类构造器中调用可重写的方法是一种危险的行为,可能会导致意想不到的结果。因为在子类构造器执行时,子类的对象还没有完全初始化完成,但此时如果调用了可重写的方法,而这个方法又依赖于子类中尚未初始化的成员变量,就可能会出现空指针异常或其他错误的行为。例如:

class ParentMethod {
    public ParentMethod() {
        // 在父类构造器中调用可重写方法
        someMethod();
    }

    public void someMethod() {
        System.out.println("父类的 someMethod 被调用");
    }
}

class ChildMethod extends ParentMethod {
    private String message;

    public ChildMethod() {
        message = "Hello";
    }

    @Override
    public void someMethod() {
        // 这里试图访问尚未初始化的 message 成员变量,会导致空指针异常
        System.out.println("子类的 someMethod 被调用,message = " + message);
    }
}

public class ConstructorAndOverrideExample {
    public static void main(String[] args) {
        try {
            ChildMethod child = new ChildMethod();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,创建 ChildMethod 类对象时,会在父类构造器调用 someMethod 方法时,由于子类的 message 成员变量还未初始化,导致空指针异常。所以,一般情况下,应该避免在子类构造器中调用可重写的方法。

346e84095c6e4eb8a080c17a0461254f.png

 

(二)正确的使用方式

宝子,这里如果确实需要在子类构造器中执行一些与父类相关但又需要子类特定实现的逻辑,可以将这些逻辑封装在一个非重写的方法中,然后在子类构造器和其他合适的地方调用这个方法,这样可以保证在对象完全初始化后再执行这些逻辑,避免出现问题。例如:

class ParentSafe {
    public ParentSafe() {
        // 调用一个非重写的初始化方法
        safeInitialization();
    }

    protected void safeInitialization() {
        System.out.println("父类的安全初始化方法被调用");
    }
}

class ChildSafe extends ParentSafe {
    private String data;

    public ChildSafe() {
        data = "World";
    }

    @Override
    protected void safeInitialization() {
        // 在这里可以安全地使用子类的成员变量
        System.out.println("子类的安全初始化方法被调用,data = " + data);
    }
}

public class SafeConstructorExample {
    public static void main(String[] args) {
        ChildSafe child = new ChildSafe();
    }
}

在这个修改后的例子中,ParentSafe 类提供了一个非重写的 safeInitialization 方法,子类 ChildSafe 重写了这个方法,并在构造器中先完成成员变量的初始化,然后再调用 safeInitialization 方法,这样就可以安全地执行子类特定的初始化逻辑,避免了前面提到的风险。

五、子类构造器在实际开发中的应用场景

(一)图形绘制系统中的子类构造器

在一个图形绘制系统中,如前所述,有一个父类 Shape,它可能包含颜色、位置等通用属性和绘制的基本方法。子类 Circle 继承自 Shape,它的构造器除了调用父类构造器初始化颜色和位置外,还需要接收圆的半径作为参数,并初始化这个特定属性。例如:

import java.awt.Color;

class Shape {
    protected Color color;
    protected int x;
    protected int y;

    public Shape(Color color, int x, int y) {
        this.color = color;
        this.x = x;
        this.y = y;
    }

    public void draw() {
        System.out.println("绘制一个基本形状");
    }
}

class Circle extends Shape {
    private int radius;

    public Circle(Color color, int x, int y, int radius) {
        super(color, x, y);
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("绘制一个圆形,半径为 " + radius + ",颜色为 " + color);
    }
}

public class GraphicsExample {
    public static void main(String[] args) {
        Circle circle = new Circle(Color.RED, 10, 20, 5);
        circle.draw();
    }
}

在这个例子中,Circle 类的构造器通过 super 关键字调用父类 Shape 的构造器来初始化颜色和位置,然后再初始化自己的半径属性,这样在绘制圆形时,就能正确地显示出圆形的所有属性。

(二)游戏角色创建中的子类构造器

在游戏开发中,假设有一个父类 Character,它定义了角色的基本属性,如生命值、攻击力等,以及一些通用的行为方法,如移动、攻击等。子类 Warrior(战士)和 Mage(法师)继承自 Character,它们的构造器可以根据各自的特点初始化不同的属性,比如战士可能有更高的生命值和近战攻击力,法师可能有更高的魔法攻击力和魔法值。例如:

class Character {
    protected int health;
    protected int attackPower;

    public Character(int health, int attackPower) {
        this.health = health;
        this.attackPower = attackPower;
    }

    public void move() {
        System.out.println("角色移动");
    }

    public void attack() {
        System.out.println("角色攻击,攻击力为 " + attackPower);
    }
}

class Warrior extends Character {
    private int defense;

    public Warrior(int health, int attackPower, int defense) {
        super(health, attackPower);
        this.defense = defense;
    }

    @Override
    public void attack() {
        System.out.println("战士强力攻击,攻击力为 " + attackPower);
    }

    public void defend() {
        System.out.println("战士防御,防御力为 " + defense);
    }
}

class Mage extends Character {
    private int magicPower;

    public Mage(int health, int attackPower, int magicPower) {
        super(health, attackPower);
        this.magicPower = magicPower;
    }

    @Override
    public void attack() {
        System.out.println("法师魔法攻击,魔法攻击力为 " + magicPower);
    }

    public void castSpell() {
        System.out.println("法师释放魔法");
    }
}

public class GameCharacterExample {
    public static void main(String[] args) {
        Warrior warrior = new Warrior(100, 20, 15);
        Mage mage = new Mage(80, 10, 30);

        warrior.move();
        warrior.attack();
        warrior.defend();

        mage.move();
        mage.attack();
        mage.castSpell();
    }
}

在这个游戏角色创建的例子中,Warrior 和 Mage 子类的构造器通过调用父类构造器初始化基本属性,并根据自身特点初始化额外的属性,如战士的防御力和法师的魔法攻击力,从而创建出具有不同特性的游戏角色,丰富了游戏的玩法和体验。

宝子们,Java 子类构造器的这些特点虽然看起来有点复杂,但只要我们深入理解并在实际开发中正确运用,就能更好地构建出高质量、健壮且易于维护的 Java 代码。希望通过这篇文章,大家对子类构造器有了更清晰、更全面的认识。如果在学习过程中还有什么疑问或者想要进一步探讨的地方,随时回来看看这篇文章,或者查阅更多的相关资料哦。

举一些子类构造器的代码示例

子类构造器和父类构造器的执行顺序是怎样的?

子类构造器可以调用父类的构造器吗?

这些都可以搜索的,文章如果不能帮到你,那是正常滴,你一定要找其他的文章,坚持学习下去,相信你一定可以的。

7bc3bfa5000445ff88fb45995608dbb2.jpeg

 

 


http://www.kler.cn/a/457588.html

相关文章:

  • ESP32 I2S音频总线学习笔记(一):初识I2S通信与配置基础
  • 被催更了,2025元旦源码继续免费送
  • 安装PyQt5-tools卡在Preparing metadata (pyproject.toml)解决办法
  • sniff2sipp: 把 pcap 处理成 sipp.xml
  • CDP集群安全指南-静态数据加密
  • 《深度学习梯度消失问题:原因与解决之道》
  • 4. 指针和动态内存
  • Pytorch | 利用PC-I-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击
  • 【13】Selenium+Python UI自动化测试 集成日志(某积载系统实例-07)
  • 【学习笔记】ChatGPT原理与应用开发——基础科普
  • No.29 笔记 | CTF 学习干货
  • C++ 设计模式:策略模式(Strategy Pattern)
  • 「Mac畅玩鸿蒙与硬件48」UI互动应用篇25 - 简易购物车功能实现
  • 【Spring】基于注解的Spring容器配置——@Scope注解
  • 如何通过采购管理系统提升供应链协同效率?
  • Android Bluetooth 问题:BluetoothAdapter enable 方法失效
  • 【2025最新计算机毕业设计】基于SpringBoot的网上服装商城系统(高质量项目,可定制)【提供源码+答辩PPT+文档+项目部署】
  • 一起来看--红黑树
  • TVS二极管选型【EMC】
  • 从0入门自主空中机器人-2-2【无人机硬件选型-PX4篇】
  • 每日一题 354. 俄罗斯套娃信封问题
  • 2025年阿斯利康GATE笔试测评春招校招社招笔试入职测评行测题型解读揭秘
  • MATLAB 车牌自动识别系统设计 SVM支持向量机方法 车牌识别
  • 代码随想录第60天
  • python opencv的sift特征检测(Scale-Invariant Feature Transform)
  • 嵌入式系统 第十二讲 块设备和驱动程序设计