📚 今日学习目标

  1. 理解为什么需要继承及其核心价值
  2. 掌握继承的语法、特点及设计原则
  3. 深入剖析子类到底能继承父类的哪些内容(构造方法、成员变量、成员方法)
  4. 理清 super 和 this 的用法与底层原理
  5. 学会方法重写的规则与应用场景

一、为什么需要继承?

在面向对象编程中,我们经常需要定义一些具有明显共性的类。例如,我们可能有一个Student类和一个Teacher类,它们都包含name、age等属性,都有eat()和sleep()等方法。

在这里插入图片描述
如果为每个类都独立编写这些相同的属性和方法,不仅代码冗余,而且维护困难。因此,我们需要一种机制来抽取共性,提高代码的复用性。这就是继承的由来。

通过继承,我们可以将这些公共的属性和方法提取到一个父类 Person类 中,然后让Student和Teacher继承这个父类,从而自动获得父类中的内容。

在这里插入图片描述


二、继承的概念与认识

2.1 什么是继承?

继承是面向对象三大特性之一封装、继承、多态)。

  • 它允许我们基于一个类(父类/基类/超类)创建一个新类(子类/派生类)。子类可以复用父类的属性和方法,并在此基础上添加新的功能。
  • 或者说可以把多个子类中重复的代码抽取到父类中。

2.2 语法:extends关键字

在Java中,通过extends关键字建立继承关系。

public class Student extends Person{}
  • 子类(派生类):Student
  • 父类(基类/超类):Person

对比c++:
C++中使用 class 子类 : 继承方式 父类 来实现继承

2.3 继承的好处

  1. 提高代码复用性:将多个子类的重复代码抽取到父类中,避免重复编写。
  2. 便于功能扩展:子类可以在父类基础上添加新功能。
  3. 形成体系:为多态提供了基础。

从父类继承过来的表现其共性,而新增的成员体现了其个性。

2.4 什么时候使用继承?

当类与类之间,存在相同的内容(共性)并满足子类是父类的一种,就可以考虑使用继承,来优化代码。

这里强调子类是父类的一种,两个类之间满足 “is-a”(是一个)关系时,才适合使用继承。

  • 学生 是一个 人 → 可以继承
  • 汽车 是一个 交通工具 → 可以继承

⚠️注意:不能仅因为有共同的行为就滥用继承。
❌ 错误示例:狗有“跑”的行为,交通工具也有“跑”的行为,就让狗继承交通工具。狗不是交通工具,“跑”只是它的一项能力。满足存在相同的内容(共性)但是并不满足子类是父类的一种。

如果将父类抽象得过于宽泛(如所有事物都继承自一个“事物”类),则粒度太粗,无法体现具体概念间的联系,反而会使设计变得混乱。正确的抽象应该聚焦于某一组相关的概念。


三、如何在代码中设计继承关系?

推荐采用 “从下往上抽取共性,从上往下实现代码” 的思路。

设计步骤

  1. 列出所有需要的类,并找出它们的共性。
  2. 画图:从子类开始,将共性逐层向上抽取,形成父类。
  3. 编码:按照从上到下的顺序(先写父类,再写子类)实现代码。

在这里插入图片描述

🌰代码示例:动物继承体系

// 动物类,所有动物都有吃饭和喝水
class Animal {
    public void eat() {
        System.out.println("吃饭");
    }
    public void drink() {
        System.out.println("喝水");
    }
}

// 猫类,继承自动物,增加抓老鼠
class Cat extends Animal {
    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}

// 狗类,继承自动物,增加看家
class Dog extends Animal {
    public void guard() {
        System.out.println("看家");
    }
}

// 布偶猫,继承猫类
class Ragdoll extends Cat {
    // 无需额外方法
}

// 中国狸花猫,继承猫类
class ChineseLiHua extends Cat {
    // 无需额外方法
}

// 哈士奇,继承狗类,增加拆家个性
class Husky extends Dog {
    public void demolishHome() {
        System.out.println("拆家");
    }
}

// 泰迪,继承狗类,增加蹭一蹭个性
class Teddy extends Dog {
    public void rub() {
        System.out.println("蹭一蹭");
    }
}

四、Java继承的特点

  1. 单继承
    Java中一个类只能有一个直接父类,即不支持多继承。这是为了避免多个父类中存在同名成员时产生的二义性。

为什么Java不支持多继承?
如果子类继承的两个父类中有相同的方法签名,子类在调用该方法时,编译器无法确定应该使用哪个父类的方法,导致混乱。🌰 例如:
在这里插入图片描述

对比C++
C++ 支持多继承:C++允许一个类继承多个父类 格式:class 子类:继承方式 父类1,继承方式父类2…,但遇到同名成员时需要加作用域区分 父类名::成员 。尽管如此,多继承在实际开发中容易引发复杂性,一般不推荐使用。

  1. 多层继承
    虽然只能有一个直接父类,但Java支持多层继承,即一个类可以继承自另一个类,而那个类又可以继承自其他类,形成继承链。
    在这里插入图片描述
    所有类最终都隐式继承自 java.lang.Object 类(如果没有显式指定父类,则默认继承Object)。因此,Object是所有类的根父类。

五、子类到底能继承父类中的哪些内容?(内存原理深度解析)

理解了继承的基本概念和特点后,我们来深入探讨一个核心问题:子类究竟能从父类得到什么? 这需要从内存角度理解。

我们需要区分 “继承” 和 “访问权限” 两个概念:

  • 继承:指子类对象的内存空间中是否包含父类的成员。
  • 访问权限:指能否使用这些成员(方法或变量)。我们这里结合继承所以谈论的是子类能否访问父类成员。

两者的关系:只有先继承了(内存中存在),才能讨论是否有权访问。

总的来说

成员类型 非私有 private
构造方法 ❌ 不能继承 ❌不能继承
成员变量 ✅ 能继承(可访问) ✅ 能继承*(不可直接访问)
成员方法 ✅ 能继承(可访问/可重写) ❌ 不能继承(也不可访问)

*注:private成员变量会被继承(存在于子类对象中),但子类无法直接访问,需要通过父类提供的公共getter/setter方法间接使用。


5.1 构造方法的继承与访问

5.1.1 继承

为什么构造方法不能被继承?

在 Java 中,构造函数的名字必须和当前类名完全一致。如果子类继承了父类的构造函数,那么子类中就存在一个名字与父类名相同的方法,这与 Java 语法冲突。

5.1.2 访问

子类如何访问父类的构造方法?

子类所有的构造方法中,第一行默认(隐式)有一条 super(); 语句,用于调用父类的无参构造方法。这是为了确保在初始化子类对象之前,先完成父类部分(父类成员变量)的初始化。

可以显式调用父类的有参构造:super(参数); 但是这条语句也必须位于子类构造方法的第一行

🌰 例如:

class Fu {
    String name;
    public Fu(String name) {  // 有参构造,无参构造不再默认提供
        this.name = name;
    }
}

class Zi extends Fu {
    public Zi(String name) {
        super(name);  // 必须显式调用父类有参构造
    }
}

this(…) 常用于构造器之间的调用
例如在无参构造器中通过 this(默认值) 调用有参构造器,从而复用初始化代码,避免重复编写。

在这里插入图片描述

而对于c++和python可以利用默认形参


5.2 成员变量的继承与访问

5.2.1 继承

无论父类的成员变量是公有还是私有,子类对象都会继承父类成员变量,在堆内存中都会为它们分配独立的内存空间。每个子类对象拥有自己的一份副本,互不影响。这与static变量有着本质区别。

在这里插入图片描述

class Fu {
    String name;  // 继承且可访问
    int age;      // 继承且可访问
}

class Zi extends Fu {
    String game;  // 子类自己的成员变量
}

public class TestStudent {
    public static void main(String[] args) {
        Zi z = new Zi();          // 堆中为Zi对象分配内存
        z.name = "铜门吹雪";       // 操作的是自己对象中的name副本
        z.age = 23;               // 操作自己对象中的age副本
        z.game = "王者农药";
        System.out.println(z.name + ", " + z.age + ", " + z.game);
    }
}

内存分配过程

  • 方法区:加载 TestStudent.class、Fu.class、Zi.class,存储类的结构信息(如方法字节码、成员变量定义等)。
  • 栈内存:main 方法执行,创建局部变量 z,存储堆中对象的地址。
  • 堆内存:执行 new Zi() 时,JVM 在堆中为 Zi 对象分配内存。这块内存既包含子类自己定义的成员变量 game,也包含从父类继承来的成员变量 name 和 age。它们按顺序连续排列,每个变量都有自己独立的空间。

继承之前我们提到的是抽取共性,不是共享!继承是“复制一份”到子类对象中,而不是“共享”。每个子类对象都拥有自己独立的父类成员变量副本,互不干扰。

特性 继承的成员变量 static 变量
内存分配 每个子类对象独有一份
其中父类成员变量和子类成员变量都是该对象独有的
所有对象共享同一份
static 变量属于类,所有对象共享同一份内存
修改影响 修改一个对象的变量不影响其他对象 修改会影响所有对象

🌰 形象比喻

  • 继承的成员变量:父亲给每个孩子100元零花钱,每个孩子都有自己的100元,独立支配。
  • static变量:父亲给家里一个公共钱包,里面放了100元,每个孩子花里面的钱都会影响其他人。

c++与此一致,私有成员变量也会继承,只是访问不到

5.2.2 访问

在这里插入图片描述
我们可以看到因为权限控制私有成员不允许访问(但是依旧会继承父类的成员变量),而公有成员允许访问。

5.2.2.1 就近原则与super/this访问

当父类和子类出现同名成员,访问同名成员时访问的是谁呢?

就近原则局部变量(方法内定义) > 本类的成员变量 > 父类的成员变量

在这里插入图片描述

public class Fu {
    String name = "Fu";
}

public class Zi extends Fu {
    String name = "Zi";

    public void ziShow() {
        String name = "ziShow";
        System.out.println(name); // 输出 ziShow
    }
}
public class Fu {
    String name = "Fu";
}

public class Zi extends Fu {
    String name = "Zi";

    public void ziShow() {
        System.out.println(name);  // 输出 Zi
    }
}
public class Fu {
    String name = "Fu";
}

public class Zi extends Fu {
    public void ziShow() {
        System.out.println(name);  // 输出 Fu
    }
}

使用this和super指定访问范围

  • this.成员变量:从本类开始找,依据就近原则,找不到就访问父类的成员变量。

  • super.成员变量:直接从父类部分开始找。
    在这里插入图片描述

详细讲解super
super它的作用是告诉编译器,在访问成员(变量或方法)时,应从当前对象的父类部分开始查找。

堆内存中的 Zi 对象:
+---------------------+
|   对象头 (mark word) |
+---------------------+
|   Fu类的成员变量     |   <-- 这部分是“父类子对象”
|   (例如 Fu.name)     |
+---------------------+
|   Zi类的成员变量     |   <-- 子类自己的成员
|   (例如 Zi.name)     |
+---------------------+

super.name:编译器从当前对象的父类部分起始位置开始查找,因此访问的是 Fu.name。是子类对象中属于父类的那部分内存,也就是子类从父类继承下来的成员变量副本。与其他子类对象无关,更是不是访问“父类对象”的空间

注意:当父类和子类成员变量没有重名时,可以直接写变量名,无需this或super。super只能指向直接父类,无法通过super.super访问祖父类。


5.3 成员方法的继承与访问

5.3.1 继承(虚方法表)

成员方法的继承与成员变量不同方法代码本身不会在每个对象中复制一份,而是所有对象共享同一份方法代码。真正决定子类能调用哪些方法的是 “虚方法表”。
在这里插入图片描述
虚方法表

  1. 每个类在加载到方法区时,都会生成一个虚方法表。
  2. 虚方法表存储了该类所有虚方法(非private、非final、非static的方法)的实际入口地址(指向方法区中的代码)。
  3. 子类的虚方法表会继承父类的虚方法表,并将父类的虚方法地址复制过来,同时添加自己特有的虚方法。如果子类重写了父类方法,则虚方法表中该方法的地址会被替换为子类重写后的方法地址。

所以当调用虚方法时,JVM 通过对象的实际类型找到对应的虚方法表,再根据方法索引快速定位到要执行的方法地址,实现动态绑定。

有没有联想到static的内存分析原理
对于static,成员变量共享一个内存地址,而成员方法无论是否static都是只有一份,static的区别在于有没有this关键字
对于继承,成员变量会被继承分配独立的内存空间,成员方法只有一份,真正决定继承的是虚方法表

5.3.2 访问

在这里插入图片描述

class Fu {
    public void fushow1() { ... }
    private void fushow2() { ... }
}
class Zi extends Fu {
    public void zishow() { ... }
}
public class TestStudent {
    public static void main(String[] args) {
        Zi z = new Zi();
        z.zishow();
        z.fushow1();
        // z.fushow2();  // 编译错误,private方法不可见
    }
}

同理,只有子类从父类继承来的方法(即子类对象内存中存在的方法),我们才讨论它能否被访问;在此基础上,访问权限决定了是否允许直接使用。

5.3.2.1 就近原则与方法重写

方法重名的两种情况

  1. 未重写(仅仅是巧合同名):如果子类定义了一个与父类方法签名相同但并非重写,则调用时依然遵循就近原则
  2. 重写(Override):当子类觉得父类的方法实现不能满足自己的需求时,可以在子类中重新定义该方法,这就是重写。

重写的规则

  1. 方法名、参数列表必须相同
  2. 访问权限必须大于父类,返回值类型必须小于父类,建议就是我们尽量保持一致即可。
  3. 使用 @override 放在重写的方法上面,校验子类重写是否正确。【即使不写,也构成重写,只不过我们强烈建议书写】
  4. 私有方法、静态方法、final方法不能被重写

注释是给程序员看的,注解是给程序员和虚拟机看的
区别重载,方法名一致,参数列表不同,返回值不做要求

重写的本质(覆盖)

在这里插入图片描述
当子类重写父类方法后,子类对象的虚方法表中,该方法的入口地址被替换为子类重写方法的地址。这意味着通过子类对象调用该方法时,永远执行的是子类的重写版本,父类的原方法被“覆盖”了。这时,不存在“就近原则”的多个候选,只有重写后的一个方法。

无论是否是重写的方法,我们可以看到super都是直接跳到父类。
使用super关键字:super.methodName()。在 Java 中是通过 invokespecial 指令实现的。这条指令在编译期就确定了目标方法(即父类的那个具体方法),并在类加载的解析阶段将其符号引用直接解析为方法区中该方法入口的直接引用(可以理解为“地址”)。运行时执行 invokespecial 时,JVM 会直接跳转到那个地址执行代码,完全不经过虚方法表,也不参与动态分派。因此可以绕过重写,直达父类。


六、this 与 super 总结

关键字 访问成员变量 访问成员方法 访问构造方法 是否可以做返回值 本质
this this.成员变量
指代当前调用对象,完整对象,访问本类成员变量
this.成员方法(..)
访问本类成员方法
this(...)
访问本类构造方法
可以 当前对象的引用(地址值)
super super.成员变量
访问父类成员变量,代表从父类开始查找成员,不代表父类存储空间
super.成员方法(..)
访问父类成员方法
super(...)
访问父类构造方法
不可以 关键字,指示从父类部分查找

注意:super不是一个引用,不存储地址值。它只是一个编译器指令,告诉编译器从当前对象的父类部分开始查找成员。因此,它不能作为返回值返回。


以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐