继承(Inheritance)
全程沿用奶茶店统一类比体系,和封装、访问修饰符、this 知识点完全串联;从问题引入到语法实现、从核心特性到常见误区逐层拆解。
先思考一个问题: 如果我们要写「珍珠奶茶类」「芋泥奶茶类「鲜果茶类」三个类,它们都有饮品名称、价格、温度这些通用属性,也都有获取价格、饮用这些通用方法。如果每个类都把这些属性和方法重新写一遍,会出现什么问题?
最直接的问题就是:代码大量重复,修改通用逻辑要改好几个地方,维护成本高,还容易改漏。 面向对象里专门用来解决「代码复用、层级抽象」问题的特性,就是继承。
一、继承是什么
💡 通俗理解
奶茶店不会给每一款饮品单独从零设计配方模板,而是先做一份「通用饮品基础模板」,上面写清楚所有饮品都有的要素:名称、定价规则、基础制作步骤。 具体的珍珠奶茶、芋泥奶茶、果茶,都直接沿用这份基础模板,只需要补充自己独有的配料、调整专属的制作步骤就行,不用从零开始写。
对应到代码里:
通用饮品模板 = 父类(基类 / 超类):抽取所有子类共有的属性和方法,是抽象的通用模板
具体奶茶品类 = 子类(派生类):继承父类的所有通用内容,再扩展自己的特有属性和方法
一句话理解:继承就是「子类复用父类的属性和方法,不用重复写,还能按需扩展」。
🔴 记忆:正式定义
继承是面向对象三大核心特性之一,是实现代码复用的核心机制。它允许一个类(子类)通过关键字继承另一个类(父类)的非私有属性与方法,子类可以直接复用父类成员,也可以定义自己的专属成员,甚至重写父类的方法逻辑。
继承的三大核心价值
代码复用,减少冗余 通用属性和方法只在父类写一次,所有子类直接复用,不用重复编写,大幅减少重复代码。
层级清晰,便于扩展 把通用逻辑向上抽取到父类,子类只关注自己的专属逻辑,结构分层清晰,新增品类时只需要继承父类、补充独有内容即可。
为多态提供基础 继承是多态的前提,没有父子类的继承关系,多态就无法实现(多态后续会详细讲)。
二、继承的基础语法
1. 核心关键字:extends
Java 中通过extends关键字实现继承,语法格式:
// 子类 extends 父类 public class 子类类名 extends 父类类名 { // 子类自己的属性和方法 }2. 基础代码示例(奶茶店场景)
先定义父类:所有饮品的通用模板
/** * 父类:基础饮品类 * 抽取所有饮品共有的属性和方法 */ public class BaseDrink { // 通用属性:所有饮品都有名称、价格、温度 private String name; private double price; private int temperature; // 通用方法:获取价格 public double getPrice() { return price; } public void setPrice(double price) { if(price < 0) { System.out.println("价格不能为负"); return; } this.price = price; } // 通用方法:饮用 public void drink() { System.out.println("饮用" + name + ",温度" + temperature + "度"); } // 通用get/set方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public int getTemperature() { return temperature; } public void setTemperature(int temperature) { this.temperature = temperature; } }再定义子类:奶茶类,继承基础饮品类
/** * 子类:奶茶类 * 继承BaseDrink,复用所有通用属性和方法,只需要补充奶茶独有的内容 */ public class MilkTea extends BaseDrink { // 子类独有属性:茶底 private String teaBase; // 子类独有方法:加小料 public void addTopping(String topping) { System.out.println("给" + getName() + "添加" + topping); } // 茶底的get/set public String getTeaBase() { return teaBase; } public void setTeaBase(String teaBase) { this.teaBase = teaBase; } }👉 效果:MilkTea类里虽然只写了茶底和加小料的方法,但它自动拥有了父类BaseDrink的 name、price、temperature 属性,以及 drink、getPrice 等所有 public 方法,创建对象后可以直接调用:
public class Test { public static void main(String[] args) { MilkTea pearlTea = new MilkTea(); // 调用父类继承来的方法 pearlTea.setName("珍珠奶茶"); pearlTea.setPrice(12); pearlTea.setTemperature(60); // 调用子类自己的方法 pearlTea.setTeaBase("红茶"); pearlTea.addTopping("珍珠"); // 调用父类继承来的方法 pearlTea.drink(); } }3. 子类到底能继承到什么?
继承不是父类所有内容都原封不动拿过来,能不能继承,完全由访问修饰符决定,和之前讲的权限规则完全对应:
| 父类成员的修饰符 | 子类能否直接访问 |
|---|---|
| public | ✅ 可以直接访问 |
| protected | ✅ 可以直接访问(继承的核心设计之一) |
| default(包私有) | 同包子类可以,不同包子类不行 |
| private | ❌ 不能直接访问 |
💡 补充:父类的 private 属性子类不是 “完全拿不到”,只是不能直接用
this.属性名访问,依然可以通过父类提供的 public get/set 方法间接操作,这也是「封装 + 继承」配合的典型场景。
三、继承的核心规则与 super 关键字
1. Java 是单继承机制
Java 中一个子类只能有一个直接父类,不支持同时继承多个类(比如不能写class A extends B, C)。 但支持多层继承:A 继承 B,B 继承 C,A 会同时拥有 B 和 C 的所有可继承成员,形成「爷爷 → 爸爸 → 儿子」的继承树。 比如:BaseDrink→MilkTea→PearlMilkTea
2. 构造方法不能被继承
父类的构造方法,子类不能继承复用。但子类构造方法执行时,默认会先调用父类的无参构造方法,相当于先初始化父类的通用属性,再初始化子类的专属属性。 这个默认调用的动作,就是靠super()实现的。
3. super 关键字:父类对象的引用
super代表当前对象中父类部分的引用,专门用来访问父类的成员,和我们之前讲的this是对应关系。 它有三大核心用法,和this一一对应:
| 用法 | 作用 |
|---|---|
super.属性名 | 访问父类的成员变量 |
super.方法名(参数) | 调用父类的成员方法 |
super(参数) | 调用父类的构造方法 |
用法 1:调用父类构造方法
子类构造方法的第一行,默认会有一个隐藏的super();,自动调用父类的无参构造。 如果父类没有无参构造(比如手动写了有参构造),子类就必须手动用super(参数)调用父类的有参构造,否则编译报错。
代码示例:
// 父类:写了有参构造,没有无参构造 public class BaseDrink { private String name; private double price; public BaseDrink(String name, double price) { this.name = name; this.price = price; } } // 子类:必须手动调用父类有参构造 public class MilkTea extends BaseDrink { private String teaBase; public MilkTea(String name, double price, String teaBase) { // 必须写在第一行,调用父类有参构造,先初始化父类属性 super(name, price); this.teaBase = teaBase; } }🔴 记忆:
super(参数)和this(参数)一样,都必须写在构造方法的第一行,所以二者不能同时出现在同一个构造方法里。
用法 2:调用父类的普通方法
子类重写父类方法后,如果还想用到父类原有的逻辑,可以用super.方法名()调用。 这个用法在方法重写里非常常用,下面讲重写的时候会结合示例。
四、方法重写(Override)
子类继承了父类的方法,但父类的通用逻辑不一定完全符合子类的需求,这时候子类可以重新实现父类的同名方法,这就是方法重写,也叫方法覆盖。
💡 通俗理解
父类的通用模板里写了「基础制作步骤」,但珍珠奶茶有自己的专属制作流程,就可以把制作方法重写一遍,保留通用部分,补充专属步骤,不用受父类逻辑的限制。
1. 重写的代码示例
// 父类 public class BaseDrink { public void make() { System.out.println("准备基础饮品原料"); } } // 子类重写make方法 public class MilkTea extends BaseDrink { // @Override 是注解,用来标记这是重写的方法,编译器会帮你检查格式是否正确 @Override public void make() { // 可以选择先调用父类的原有逻辑,也可以完全重写 super.make(); System.out.println("加入茶底和牛奶"); System.out.println("添加配料摇匀"); } }2. 重写的核心规则(面试高频)
重写不是随便写个同名方法就行,必须遵守以下规则,否则就不是重写:
- 方法签名必须一致:方法名、参数列表、参数顺序必须和父类完全相同
- 返回值类型一致:基本类型必须相同,引用类型可以是父类返回值的子类
- 访问权限不能更严格:父类是 public,子类就不能写成 protected 或 private
- 抛出的异常不能更大:子类方法不能抛出比父类更宽泛的异常类型
3. 重写 vs 重载:别再混淆
很多不熟悉的同学容易把重写和重载搞混,二者完全不是一个维度的概念,对比表一目了然:
| 对比项 | 方法重写(Override) | 方法重载(Overload) |
|---|---|---|
| 适用场景 | 父子类之间 | 同一个类内部 |
| 方法签名 | 必须完全一致 | 方法名相同,参数列表必须不同 |
| 返回值 | 必须一致(或子类) | 无要求 |
| 访问权限 | 不能更严格 | 无要求 |
| 核心作用 | 子类重定义父类方法逻辑,实现运行时多态 | 同一个方法名适配不同参数,提升易用性 |
🔴 记忆:一句话区分 —— 重写是父子类的 “覆盖”,重载是同个类的 “同名不同参”。
五、所有类的根父类:Object
Java 中有一个特殊的类:java.lang.Object,它是所有类的顶级父类。 哪怕你写类的时候没写extends,编译器也会默认让它继承 Object 类。也就是说,所有 Java 对象都拥有 Object 类中定义的方法,最常用的有三个:
toString():把对象转成字符串表示equals():判断两个对象内容是否相等hashCode():返回对象的哈希值
这些方法所有类都能直接用,也可以根据需求重写,比如我们经常重写实体类的toString()和equals()方法,就是继承自 Object 的重写应用。
六、不熟悉的同学容易踩的认知误区
误区 1:继承就是把父类的代码复制粘贴到子类里
✅ 正解:不是复制,是复用引用。子类对象里包含了一个完整的父类对象结构,二者是包含关系,不是复制关系,父类修改后所有子类都会同步生效。
误区 2:private 属性不能被继承,所以子类完全没有
✅ 正解:private 属性子类不能直接访问,但它依然存在于子类对象中,只是受封装保护,只能通过父类的 public 方法间接操作。
误区 3:继承越多、层级越深越好
✅ 正解:继承会增强类之间的耦合度 —— 父类修改会影响所有子类,层级过深会导致代码难以维护。 继承只适合真正的「is-a」关系(奶茶是饮品),不要为了单纯复用代码就强行用继承,复用代码还可以用组合、工具类等更灵活的方式。
误区 4:只要方法名一样就是重写
✅ 正解:必须满足参数列表、返回值、访问权限等所有规则才是重写,建议加上@Override注解,让编译器帮你校验格式,避免写错了自己还不知道。
七、和之前知识点的串联
- 关联封装:父类的私有属性靠封装隐藏,子类通过公共 get/set 方法间接访问,二者配合实现「内部数据保护 + 外部代码复用」。
- 关联访问修饰符:
protected权限就是专门为继承设计的 —— 让子类能访问父类成员,同时对外保持隐藏。 - 关联 this 关键字:
this指向当前对象本身,super指向当前对象的父类部分,二者配合完成对象内部的成员访问。 - 关联方法重载:重载是同类内的多同名方法,重写是父子类的方法覆盖,二者是完全不同的语法特性。