【JavaSE】继承中内存发生了什么变化?这篇文章带你深究继承本质,一次搞懂~

本专栏为 JavaSE 的学习笔记及相关项目,专栏长期免费更新 ❤️ ❤️ ❤️

❤️ 个人主页:Nezuko627 欢迎来访~~~
⭐️往期回顾:

  • 【JavaSE】面向对象之封装
  • 【JavaSE】Java中的可变参数
    【JavaSE】继承中内存发生了什么变化?这篇文章带你深究继承本质,一次搞懂~_第1张图片

文章目录

  • 1 继承引入
    • 1.1 继承的基本介绍与语法
    • 1.2 继承的示意图举例
    • 1.3 继承的快速入门
  • 2 继承的深入讨论
    • 2.1 继承的细节
    • 2.2 super 的使用及代码优化
    • 2.3 super 的使用细节
  • 3 (重点)继承的本质
  • 写在最后


1 继承引入

✈️ 在实际开发中,我们常常需要使用到相同的“功能”,比如:对于小学生和大学生,都有成绩、姓名、性别、年龄这些基本属性,如果我们已经定义过了小学生类,当我们需要定义大学生类的时候,又要重新定义成绩、姓名、性别、年龄这些属性,虽然这样是可行的,但是对于程序员来说,这样无疑增加了工作量(手动狗头),高情商的说法是:这样做代码的复用性不高!!!因此,我们需要继承。

1.1 继承的基本介绍与语法

继承可以帮助我们解决代码复用性不高的问题。当多个类存在相同的属性和方法时,我们可以通过继承从抽象出的父类(即被继承的类)中继承在父类中定义过的属性和方法。即,所有的子类(继承父类的类)不需要重新定义这些属性和方法,只需要通过关键字 extends 来声明继承父类就 ok!
继承的基本语法如下:

class 子类 extends 父类{
	// 子类的定义
}

当继承关系确定后,子类就会自动拥有父类定义的属性和方法!
【注意】父类又叫超类、基类;子类也叫派生类。

1.2 继承的示意图举例

我们来看下面这个继承关系图:
【JavaSE】继承中内存发生了什么变化?这篇文章带你深究继承本质,一次搞懂~_第2张图片
在该图中,我们定义了一个父类 Person类 ,包含姓名、电话、邮箱这几个基本属性。对于学生来说,即 Student类 除了 Person类中的这些属性外,还有学号、平均成绩这些属性,因此 Student类只需要继承 Person类 后再定义学号与平均成绩的属性即可。而对于教授来说,即 Professor类 其相较 Person类又需要工资这一属性,同样地,Professor类 只需要继承 Person类 后再定义薪水这一属性即可。

1.3 继承的快速入门

我们来看一个需求(记得使用继承来解决哦!):

请你定义一个父类 Person类,包含姓名、电话和邮箱三个基本属性。由其可以派生出学生类和教授类,其中学生类增加学号、平均分两个属性,并提供展示信息(所有属性)的方法;教授类增加薪水信息,并也提供一个展示信息的方法。最后,请你提供相应的信息,并对其进行测试。

参考代码如下:(务必亲手尝试后再看!)
Tips:这里把类分开写,存在不同的 java文件中,因此可以均定义为 public,如果你是写在一个文件中,记得只保留测试类的 public。

  • Person类:
public class Person {
    String name;  // 姓名
    String phoneNumber;  // 电话
    String emailAddress;  // 邮箱
}
  • Student类
public class Student extends Person{
    private String studentNumber;  // 学号
    private double averageMark;  // 平均成绩
    // 构造方法, 便于赋值
    // 因为还没有学 super,因此,父类属性我们还是一一调用
    public Student(String studentNumber, double averageMark){
        setStudentNumber(studentNumber);
        setAverageMark(averageMark);
    }
    // set方法, 结合实际进行数据检验
    public void setStudentNumber(String studentNumber) {
        this.studentNumber = studentNumber;
    }

    public void setAverageMark(double averageMark) {
        if(averageMark >= 0)
            this.averageMark = averageMark;
        else {
            this.averageMark = 0;
            System.out.println("成绩输入有误!使用默认值0");
        }
    }

    // 展示信息的方法
    public void showStudentMessage(){
        System.out.println("========我是学生!!========");
        System.out.println("姓名: " + name);
        System.out.println("电话: " + phoneNumber);
        System.out.println("邮箱: " + emailAddress);
        System.out.println("学号: " + studentNumber);
        System.out.println("平均成绩: " + averageMark);
    }
}
  • Professor类
public class Professor extends Person{
    private int salary;  // 工资
    // 构造方法
    public Professor(int salary){
        setSalary(salary);
    }
    // 数据检验
    public void setSalary(int salary) {
        if(salary >= 0)
            this.salary = salary;
        else 
            this.salary = 0;
    }
    // 展示信息
    public void showProfessorMessage(){
        System.out.println("========我是教授!!========");
        System.out.println("姓名: " + name);
        System.out.println("电话: " + phoneNumber);
        System.out.println("邮箱: " + emailAddress);
        System.out.println("工资: " + salary + " RMB");
    }
}
  • Main类(测试用)
public class Main {
    public static void main(String[] args) {
        // 学生信息录入
        Student s = new Student("20200420", 95.4);
        s.name = "无敌霸王龙";
        s.emailAddress = "[email protected]";
        s.phoneNumber = "+8613513513522";
        // 教授信息录入
        Professor p = new Professor(15000);
        p.name = "无敌毛毛虫";
        p.emailAddress = "[email protected]";
        p.phoneNumber = "4008-823-823";
        // 信息分别展示
        s.showStudentMessage();
        p.showProfessorMessage();
    }
}

运行结果如下:
【JavaSE】继承中内存发生了什么变化?这篇文章带你深究继承本质,一次搞懂~_第3张图片


2 继承的深入讨论

2.1 继承的细节

⭐️ star1: 子类继承了父类所有的属性和方法,但是 父类的私有属性和方法不能在子类中直接访问,要通过父类提供公共的方法去访问。 比如在上面的例子中,我们把 Person类 的 name 设置为 private ,则子类在使用该属性时,需要父类提供一个 public 方法用于让子类等访问私有属性。

⭐️ star2: 子类必须调用父类的构造器完成父类的初始化。

⭐️ star3: 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器。 如果父类没有提供无参构造器,则必须在子类中的构造器使用 super 去指明使用父类的哪个构造器完成对父类的初始化工作,否则,编译不能通过!!

我们还是举刚刚的例子,我们给 Person类添加个无参构造方法

  • Person 类:
public class Person {
    String name;  // 姓名
    String phoneNumber;  // 电话
    String emailAddress;  // 邮箱
    // 父类的无参构造器
    public Person(){
        System.out.println("我是Person的无参构造器!");
    }
}

Main类我们保持不变,我们对程序继续进行测试,发现结果多出了两行!
【JavaSE】继承中内存发生了什么变化?这篇文章带你深究继承本质,一次搞懂~_第4张图片
简单分析一下,在我们实例化 Student对象 和 Professor对象时,使用的是子类的有参构造器,但是在创建子类实例化对象的过程中 系统自动调用了父类的无参构造器!!!
如果我们将Person类的无参构造器给无意义化,即只写一个有参构造方法但是在子类的构造器中不去调用有参构造器:

  • Person类:
public class Person {
    String name;  // 姓名
    String phoneNumber;  // 电话
    String emailAddress;  // 邮箱
    // 父类的有参构造器
    public Person(int name){
        System.out.println("我是Person的有参构造器!");
    }
}

此时我们再运行,编译不能通过!!!
【JavaSE】继承中内存发生了什么变化?这篇文章带你深究继承本质,一次搞懂~_第5张图片
嘿嘿,那么怎么做才能使编译通过呢?其实你可以在父类中再声明一个无参构造器,但是笔者这里想介绍的是另一种方法:使用 super,很快啊,说来就来了!见 本文 2.2小节 这里我们还是先继续聊 继承的细节

⭐️ star4: Java 中所有类都是 Object类 的子类, Object类 是所有类的基类。父类的构造器调用不限于父类,将一直追溯到 Object类!(这点其实你懂得 star3 的内容,就理解了,带着问题,看 2.2 小节,相信你有意想不到的收获!)

⭐️ star5: (重要) 子类最多只能继承一个父类(直接继承),即 Java 是单继承机制

2.2 super 的使用及代码优化

  • super 简单介绍:

super(); // 调用父类的无参构造器
super(参数列表); // 调用父类的有参构造器

⭐️代码优化如下注意观察父类与子类构造方法的变化!

  • Person类:
public class Person {
    String name;  // 姓名
    String phoneNumber;  // 电话
    String emailAddress;  // 邮箱
    // 有参构造方法
    public Person(String name, String phoneNumber, String emailAddress) {
        this.name = name;
        this.phoneNumber = phoneNumber;
        this.emailAddress = emailAddress;
    }
}
  • Student类:
public class Student extends Person{
    private String studentNumber;  // 学号
    private double averageMark;  // 平均成绩
    // 构造方法, 便于赋值
    public Student(String name, String phoneNumber, String emailAddress, String studentNumber, double averageMark) {
        super(name, phoneNumber, emailAddress);
        setStudentNumber(studentNumber);
        setAverageMark(averageMark);
    }

    // set方法, 结合实际进行数据检验
    public void setStudentNumber(String studentNumber) {
        this.studentNumber = studentNumber;
    }

    public void setAverageMark(double averageMark) {
        if(averageMark >= 0)
            this.averageMark = averageMark;
        else {
            this.averageMark = 0;
            System.out.println("成绩输入有误!使用默认值0");
        }
    }

    // 展示信息的方法
    public void showStudentMessage(){
        System.out.println("========我是学生!!========");
        System.out.println("姓名: " + name);
        System.out.println("电话: " + phoneNumber);
        System.out.println("邮箱: " + emailAddress);
        System.out.println("学号: " + studentNumber);
        System.out.println("平均成绩: " + averageMark);
    }
}
  • Professor类:
public class Professor extends Person{
    private int salary;
    // 构造方法
    public Professor(String name, String phoneNumber, String emailAddress, int salary) {
        super(name, phoneNumber, emailAddress);
        setSalary(salary);
    }
    // 数据检验
    public void setSalary(int salary) {
        if(salary >= 0)
            this.salary = salary;
        else
            this.salary = 0;
    }
    // 展示信息
    public void showProfessorMessage(){
        System.out.println("========我是教授!!========");
        System.out.println("姓名: " + name);
        System.out.println("电话: " + phoneNumber);
        System.out.println("邮箱: " + emailAddress);
        System.out.println("工资: " + salary + " RMB");
    }
}
  • Main测试类:
public class Main {
    public static void main(String[] args) {
        // 学生信息录入
        Student s = new Student("无敌霸王龙", "+8613513513522","20200420",
                "[email protected]", 95.4);
        // 教授信息录入
        Professor p = new Professor("无敌毛毛虫", "4008-823-823", 
        		"[email protected]", 15000);
        // 信息分别展示
        s.showStudentMessage();
        p.showProfessorMessage();
    }
}

此时,运行结果与之前讲述的例子一致,就不放图啦!能感受到的明显的区别就是:Main测试函数更加简洁了,只需要用构造器就可以一次性给属性赋值!

2.3 super 的使用细节

⭐️ star1: super 在使用时必须放在构造器的第一行。
⭐️ star2: super() 与 this()均要放在构造器的第一行,所以,两者不能共同存在于一个构造器中!
⭐️ star3: super 只能在构造器中使用,不能在成员方法中使用!


3 (重点)继承的本质

✈️ 在这里,我们举例,并通过内存的角度分析理解,当我们在创建子类对象时,内存中到底发生了什么?

案例代码如下:

class GrandPa{
    String name = "大头爷爷";
    String hobby = "旅游";
}

class Father extends  GrandPa{
    String name = "大头爸爸";
    int age = 39;
}
class Son extends Father{
    String name = "大头儿子";
}

// 测试
public class ExtendsTheory {
    public static void main(String[] args) {
        Son son = new Son();
    }
}

当程序执行到 new Son() 时,虚拟机会在方法区加载类相关信息。按照 父类优先 的顺序,依次加载 Object类GrandPa类Father类Son类 的类信息。

  1. 堆区分配对象需要的内存,包括本类和父类的所有实例变量,但不包括任何静态变量;
  2. 对所有实例变量 赋默认值;
  3. ==执行实例化初始代码:先初始化父类再初始化子类。==此时在堆区分配空间 OX11 用于存储 son对象,里面包含 GrandPa类(绿色),Father类(紫色),Son类(粉色)。其中,每个区域都根据基本数据类型与引用数据类型的机制,在堆区或者常量池存储相应的值;
    ❤️ Tips:如果对这句话有不理解的,就戳进这篇博客:【JavaSE】深入理解类与对象 || 方法调用机制与方法的传参机制浅析
  4. 栈区开辟一个 son对象的引用,存储指向对象所在的堆区的地址。

具体图示如下:
【JavaSE】继承中内存发生了什么变化?这篇文章带你深究继承本质,一次搞懂~_第6张图片
如果我们将代码改成下面这样,程序会输出什么呢?

class GrandPa{
    String name = "大头爷爷";
    String hobby = "旅游";
}

class Father extends  GrandPa{
    String name = "大头爸爸";
    int age = 39;
}
class Son extends Father{
    String name = "大头儿子";
}

// 测试
public class ExtendsTheory {
    public static void main(String[] args) {
        Son son = new Son();
        System.out.println(son.name);
        System.out.println(son.age);
        System.out.println(son.hobby);
    }
}

结果如下:
【JavaSE】继承中内存发生了什么变化?这篇文章带你深究继承本质,一次搞懂~_第7张图片
分析?其实在访问对象时遵循以下规则:

(1)首先看子类是否有该属性
(2)如果子类有这个属性,并且可以访问,则返回信息
(3)如果子类没有这个属性,就看父类有没有这个属性(如果父类有该属性,并且可以访问,就返回信息…)
(4)如果父类没有就按照(3)的规则,继续找上级父类,直到Object… .


写在最后

以上便是本文的全部内容啦,后续内容将会持续免费更新,如果文章对你有所帮助,麻烦动动小手点赞 + 收藏 + 关注,非常感谢 ❤️ ❤️!
【JavaSE】继承中内存发生了什么变化?这篇文章带你深究继承本质,一次搞懂~_第8张图片

如果有问题,欢迎私信或者评论区!
共勉:“其实一直陪伴你的,是那个了不起的自己。”
在这里插入图片描述

你可能感兴趣的:(JavaSE,java)