【第五章】Java------继承、抽象类与多态
- 一、为什么要学习继承、抽象类与多态
-
- [1. 单纯使用类和对象还不够](#1. 单纯使用类和对象还不够)
- [2. 代码复用的需求非常强烈](#2. 代码复用的需求非常强烈)
- [3. 现实世界中天然存在"is-a"关系](#3. 现实世界中天然存在“is-a”关系)
- [4. 有些父类只想拿来约束,不想直接创建对象](#4. 有些父类只想拿来约束,不想直接创建对象)
- [5. 程序不仅要复用,还要可扩展](#5. 程序不仅要复用,还要可扩展)
- 二、什么是继承
-
- [1. 继承的基本概念](#1. 继承的基本概念)
- [2. 继承的本质](#2. 继承的本质)
- [3. 继承的意义](#3. 继承的意义)
- 三、继承的基本语法
-
- [1. 语法格式](#1. 语法格式)
- [2. 示例](#2. 示例)
- 四、子类到底继承了父类什么
-
- [1. 子类会继承父类的非私有成员](#1. 子类会继承父类的非私有成员)
- [2. 构造方法不能被继承](#2. 构造方法不能被继承)
- [3. 私有成员不能被子类直接访问](#3. 私有成员不能被子类直接访问)
- [4. 静态成员会被继承吗](#4. 静态成员会被继承吗)
- 五、继承中的成员访问特点
-
- [1. 成员变量的访问](#1. 成员变量的访问)
- [2. 如果想访问父类同名成员变量怎么办](#2. 如果想访问父类同名成员变量怎么办)
- [3. 方法访问也遵循查找机制](#3. 方法访问也遵循查找机制)
- [六、`super` 关键字详解](#六、
super关键字详解) -
- [1. `super` 的含义](#1.
super的含义) - [2. `super` 的常见用法](#2.
super的常见用法) - [3. `super` 访问父类方法示例](#3.
super访问父类方法示例) - [4. `this` 与 `super` 的区别](#4.
this与super的区别)
- [1. `super` 的含义](#1.
- 七、子类构造方法与父类构造方法
-
- [1. 子类对象创建时,一定会先调用父类构造方法](#1. 子类对象创建时,一定会先调用父类构造方法)
- [2. 默认会隐含调用 `super()`](#2. 默认会隐含调用
super()) - [3. 如果父类没有无参构造怎么办](#3. 如果父类没有无参构造怎么办)
- [4. `super(...)` 必须写在构造方法第一行](#4.
super(...)必须写在构造方法第一行) - [5. `this(...)` 与 `super(...)` 不能同时出现在同一构造方法第一行](#5.
this(...)与super(...)不能同时出现在同一构造方法第一行)
- 八、方法重写
-
- [1. 什么是方法重写](#1. 什么是方法重写)
- [2. 为什么要重写](#2. 为什么要重写)
- [3. 方法重写的条件](#3. 方法重写的条件)
- [4. `@Override` 注解](#4.
@Override注解) - [5. 方法重写示例](#5. 方法重写示例)
- 九、方法重写与方法重载的区别
- 十、什么是抽象类
-
- [1. 抽象类的基本概念](#1. 抽象类的基本概念)
- [2. 抽象类的语法](#2. 抽象类的语法)
- [3. 抽象类不能直接创建对象](#3. 抽象类不能直接创建对象)
- [4. 抽象类里可以有普通成员](#4. 抽象类里可以有普通成员)
- [5. 什么是抽象方法](#5. 什么是抽象方法)
- [6. 抽象类中可以有抽象方法,也可以没有](#6. 抽象类中可以有抽象方法,也可以没有)
- 十一、抽象类和继承、多态之间是什么关系
-
- [1. 抽象类本质上还是父类](#1. 抽象类本质上还是父类)
- [2. 子类继承抽象类后,要么重写全部抽象方法,要么自己也声明为抽象类](#2. 子类继承抽象类后,要么重写全部抽象方法,要么自己也声明为抽象类)
- [3. 抽象方法经常是在"强制子类重写"](#3. 抽象方法经常是在“强制子类重写”)
- [4. 抽象类常常是多态最自然的父类型](#4. 抽象类常常是多态最自然的父类型)
- 十二、什么是多态
-
- [1. 多态的基本概念](#1. 多态的基本概念)
- [2. 生活中的类比](#2. 生活中的类比)
- [3. Java 中的多态表现](#3. Java 中的多态表现)
- 十三、多态成立的条件
-
- [1. 要有继承或实现关系](#1. 要有继承或实现关系)
- [2. 要有方法重写](#2. 要有方法重写)
- [3. 要有父类引用指向子类对象](#3. 要有父类引用指向子类对象)
- [4. 示例](#4. 示例)
- 十四、父类引用指向子类对象怎么理解
-
- [1. 看起来很奇怪,实际上很合理](#1. 看起来很奇怪,实际上很合理)
- [2. 为什么父类能接收子类对象](#2. 为什么父类能接收子类对象)
- [3. 这种设计有什么好处](#3. 这种设计有什么好处)
- 十五、多态下访问成员变量和成员方法的规律
-
- [1. 成员变量访问规律](#1. 成员变量访问规律)
- [2. 成员方法访问规律](#2. 成员方法访问规律)
- [3. 经典总结](#3. 经典总结)
- [4. 口诀总结](#4. 口诀总结)
- 十六、多态的好处
-
- [1. 提高程序扩展性](#1. 提高程序扩展性)
- [2. 降低代码耦合度](#2. 降低代码耦合度)
- [3. 让代码更通用](#3. 让代码更通用)
- [4. 符合开闭原则思想](#4. 符合开闭原则思想)
- 十七、多态中的向上转型
-
- [1. 什么是向上转型](#1. 什么是向上转型)
- [2. 为什么叫"向上"](#2. 为什么叫“向上”)
- [3. 向上转型是否需要强制转换](#3. 向上转型是否需要强制转换)
- [4. 向上转型后的特点](#4. 向上转型后的特点)
- 十八、多态中的向下转型
-
- [1. 什么是向下转型](#1. 什么是向下转型)
- [2. 为什么需要向下转型](#2. 为什么需要向下转型)
- [3. 向下转型需要强制类型转换](#3. 向下转型需要强制类型转换)
- [4. 向下转型有风险](#4. 向下转型有风险)
- [5. 安全转型要配合 `instanceof`](#5. 安全转型要配合
instanceof)
- [十九、`instanceof` 关键字详解](#十九、
instanceof关键字详解) -
- [1. 作用](#1. 作用)
- [2. 示例](#2. 示例)
- [3. 为什么它很重要](#3. 为什么它很重要)
- 二十、一个完整的继承、抽象类与多态案例
- 二十一、继承的优点与局限
-
- [1. 优点](#1. 优点)
- [2. 局限](#2. 局限)
- [3. 使用继承的原则](#3. 使用继承的原则)
- 二十二、多态的应用思想
-
- [1. 面向父类编程](#1. 面向父类编程)
- [2. 统一调用,不同实现](#2. 统一调用,不同实现)
- [3. 方便后续扩展](#3. 方便后续扩展)
- 二十三、开发中常见错误与误区
-
- [1. 误以为继承可以继承父类一切内容](#1. 误以为继承可以继承父类一切内容)
- [2. 误以为子类对象创建时只执行子类构造](#2. 误以为子类对象创建时只执行子类构造)
- [3. 误以为方法重写和重载是一回事](#3. 误以为方法重写和重载是一回事)
- [4. 误以为多态下变量和方法都看右边](#4. 误以为多态下变量和方法都看右边)
- [5. 误以为父类引用能直接调用子类特有方法](#5. 误以为父类引用能直接调用子类特有方法)
- [6. 不加判断就强制向下转型](#6. 不加判断就强制向下转型)
- [7. 滥用继承](#7. 滥用继承)
- 二十四、学习继承、抽象类与多态的建议
-
- [1. 先把继承关系画出来](#1. 先把继承关系画出来)
- [2. 多画内存和引用关系](#2. 多画内存和引用关系)
- [3. 多写重写案例](#3. 多写重写案例)
- [4. 遇到题目先问自己四个问题](#4. 遇到题目先问自己四个问题)
- 二十五、本章知识总结
-
- [1. 继承总结](#1. 继承总结)
- [2. 抽象类总结](#2. 抽象类总结)
- [3. 重写总结](#3. 重写总结)
- [4. 多态总结](#4. 多态总结)
- [5. 成员访问规律总结](#5. 成员访问规律总结)
- [6. 类型转换总结](#6. 类型转换总结)
- 二十六、结语
- 二十七、附:常见语法速查表
-
- [1. 继承语法](#1. 继承语法)
- [2. 抽象类与抽象方法](#2. 抽象类与抽象方法)
- [3. `super` 常见写法](#3.
super常见写法) - [4. 方法重写写法](#4. 方法重写写法)
- [5. 多态写法](#5. 多态写法)
- [6. 向下转型写法](#6. 向下转型写法)
- [7. `instanceof` 写法](#7.
instanceof写法)
前言
在 Java 面向对象编程中,继承 、抽象类 与 多态 是一组联系非常紧的知识点。前面学类和对象时,更多是在做"抽象";到了这一章,重点就变成了:
- 类与类之间怎样建立层次关系
- 代码如何实现复用
- 有些父类为什么只适合拿来"约束",不适合直接创建对象
- 相同操作为什么会表现出不同结果
- 为什么父类引用可以指向子类对象
- 为什么同样是调用一个方法,运行结果却和对象真实类型有关
很多初学者在刚学到这里时,会觉得概念很多、关系很绕,常见困惑包括:
- 什么叫"子类继承父类"
- 子类到底继承了父类的什么内容
- 构造方法能不能被继承
super和this有什么区别- 抽象类到底"抽象"在哪里
- 抽象类和普通父类有什么区别
- 抽象方法为什么没有方法体
- 方法重写和方法重载到底哪里不一样
- 父类引用指向子类对象是什么意思
- 编译看左边、运行看右边到底怎么理解
- 为什么会有向上转型和向下转型
- 为什么多态能提高程序扩展性
这些问题,基本都集中在"继承、抽象类与多态"这一章里。
可以这样理解:
继承负责抽取共性,抽象类负责把共性规范再往前推进一步,多态负责让同一种调用在不同对象上表现出不同结果。
这三者不是割裂开的。
很多时候,先有继承关系,再把父类设计成抽象类,最后通过父类引用指向子类对象来体现多态,这才是比较完整的一条线。
本章会按"继承 -> 抽象类 -> 多态"这条主线来讲,尽量把语法、运行逻辑、常见坑和代码案例串在一起,避免只背概念却不会分析代码。
通过本章学习,可以掌握:
- 什么是继承
- 继承的基本语法
- 子类能够继承父类哪些成员
extends的作用super关键字的使用方法- 什么是抽象类
- 抽象类和普通类的区别
- 抽象方法的定义方式
- 子类继承抽象类后的规则
- 方法重写的概念、条件和注意事项
- 继承中成员访问的查找顺序
- 什么是多态
- 多态成立的条件
- 父类引用指向子类对象的本质
- 多态下成员变量和成员方法的访问规律
- 向上转型与向下转型
instanceof的作用- 继承与多态在实际开发中的意义
一、为什么要学习继承、抽象类与多态
1. 单纯使用类和对象还不够
在前面的面向对象学习中,我们已经知道:
- 类用于描述一类事物
- 对象是类的具体实例
例如:
Student类表示学生Teacher类表示老师Dog类表示狗Cat类表示猫
但是继续往下写程序时,就会发现一个问题:
很多类之间并不是完全独立的,它们往往存在共同特征。
例如:
- 学生和老师都属于"人"
- 狗和猫都属于"动物"
- 圆形和矩形都属于"图形"
- 支付宝支付和微信支付都属于"支付方式"
如果每个类都从零开始写,往往会产生大量重复代码。
2. 代码复用的需求非常强烈
例如:
- 学生有姓名、年龄、性别
- 老师也有姓名、年龄、性别
如果 Student 和 Teacher 中都重复定义这些属性和方法,就会造成:
- 代码冗余
- 维护困难
- 修改麻烦
这时候就会自然想到:
能不能把它们共同的内容抽取到一个更通用的类中,然后让其他类复用?
这正是继承要解决的问题。
3. 现实世界中天然存在"is-a"关系
继承最适合表达一种关系:
子类 is a 父类
例如:
- 学生是一个人
- 老师是一个人
- 狗是一种动物
- 汽车是一种交通工具
只要这种"是一个"的关系成立,就往往可以考虑使用继承。
4. 有些父类只想拿来约束,不想直接创建对象
实际开发里,经常会碰到这样一种类:
- 它确实适合作为父类
- 它里面确实写了一些共性成员
- 但这个类本身又不够具体,不应该直接
new
例如:
Animal可以作为狗、猫、鸟的父类- 但"动物"这个概念太泛,很多时候不适合直接创建一个"纯动物对象"
Shape可以作为圆形、矩形、三角形的父类- 但"图形"往往只是一个抽象总类
这时候,普通继承就还差一点意思。
因为普通父类既能被继承,也能被直接实例化。
如果你不希望别人直接拿它创建对象,就需要引入抽象类。
5. 程序不仅要复用,还要可扩展
除了复用代码,程序还要有良好的扩展能力。例如写一个"动物叫"的程序:
- 狗会汪汪叫
- 猫会喵喵叫
- 鸟会啾啾叫
如果每增加一种动物,都要修改一大堆旧代码,那么程序就很僵硬。
这时就需要多态,让程序在面对不同对象时,自动表现出不同的行为。
二、什么是继承
1. 继承的基本概念
继承是面向对象的三大特征之一。在 Java 中,继承表示:
让一个类获得另一个类中已有的属性和方法。
被继承的类称为:
- 父类
- 基类
- 超类
继承别人的类称为:
- 子类
- 派生类
例如:
java
class Person {
String name;
int age;
public void eat() {
System.out.println("人会吃饭");
}
}
class Student extends Person {
public void study() {
System.out.println("学生会学习");
}
}
这里:
Person是父类Student是子类Student通过extends继承了Person
因此 Student 不仅有自己的 study() 方法,还能使用从 Person 继承来的:
nameageeat()
2. 继承的本质
继承的本质并不是"复制粘贴"父类代码,而是建立一种类与类之间的层次关系。
这种关系使得子类拥有父类非私有的成员访问能力,并可以在此基础上扩展自己的新功能。
3. 继承的意义
继承的主要意义有:
- 提高代码复用性
- 减少重复代码
- 建立类之间的层次结构
- 为多态提供前提条件
三、继承的基本语法
1. 语法格式
Java 中使用 extends 关键字表示继承。
java
class 子类名 extends 父类名 {
// 子类自己的成员
}
2. 示例
java
class Animal {
String name;
public void eat() {
System.out.println("动物会吃东西");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("狗会汪汪叫");
}
}
使用时:
java
public class Test {
public static void main(String[] args) {
Dog d = new Dog();
d.name = "旺财";
d.eat();
d.bark();
}
}
输出结果:
text
动物会吃东西
狗会汪汪叫
这说明:
Dog自己新增了bark()- 同时还能使用
Animal的成员
四、子类到底继承了父类什么
这是非常重要的问题。
1. 子类会继承父类的非私有成员
通常可以先记住一个结论:
子类会继承父类中可访问的成员变量和成员方法。
这些成员包括:
- 成员变量
- 成员方法
但这里有一个前提:访问权限要允许。
2. 构造方法不能被继承
很多初学者容易误以为"子类把父类的一切都继承了"。
实际上,构造方法不能被继承。
原因很简单:
- 构造方法的名字必须和类名一致
- 父类构造方法名字是父类名
- 子类构造方法名字必须是子类名
因此从语法本质上,它就不能被直接继承。
不过要注意:
虽然构造方法不能被继承,但子类对象创建时会调用父类构造方法。
这一点后面会详细讲。
3. 私有成员不能被子类直接访问
如果父类成员使用 private 修饰,那么子类不能直接访问。
例如:
java
class Father {
private int age = 50;
}
class Son extends Father {
public void test() {
// System.out.println(age); // 这里会报错
}
}
这并不意味着父类私有成员"完全不存在",而是:
- 它们属于父类对象的一部分
- 但子类不能直接使用名字访问它们
如果父类提供了公共的 get / set 方法,子类仍可间接操作。
4. 静态成员会被继承吗
从语法使用上看,子类可以访问父类的静态成员。
但静态成员本质上属于类,而不属于某一个对象。
因此更准确地说:
- 静态成员会被子类"继承可访问性"
- 但它并不是对象实例层面的普通成员
初学阶段可以先记住:
- 父类的静态成员,子类可以使用
- 但最好通过"类名.成员"访问,而不是通过对象访问
五、继承中的成员访问特点
1. 成员变量的访问
当子类和父类中出现同名成员变量时,访问会遵循就近原则。
示例:
java
class Father {
int num = 10;
}
class Son extends Father {
int num = 20;
public void show() {
System.out.println(num);
}
}
输出的是:
text
20
因为在 show() 方法内部,优先访问的是当前类中的成员变量。
2. 如果想访问父类同名成员变量怎么办
这时候就需要使用 super。
java
class Father {
int num = 10;
}
class Son extends Father {
int num = 20;
public void show() {
System.out.println(num); // 20
System.out.println(super.num); // 10
}
}
这里:
num表示当前对象中优先从子类查找的成员super.num表示访问父类中的成员变量
3. 方法访问也遵循查找机制
如果子类没有该方法,就向父类找。
如果父类也没有,再继续向更上层找。
如果一直找不到,就编译报错。
例如:
java
class Father {
public void eat() {
System.out.println("父类吃饭");
}
}
class Son extends Father {
public void study() {
System.out.println("子类学习");
}
}
Son 对象既可以调用:
study()eat()
因为 eat() 能在父类中找到。
六、super 关键字详解
super 是继承中必须熟练掌握的关键字。
1. super 的含义
super 可以理解为:
当前对象中的父类部分。
它主要用于在子类中访问父类成员。
2. super 的常见用法
(1)访问父类成员变量
java
super.成员变量名
(2)访问父类成员方法
java
super.成员方法名();
(3)调用父类构造方法
java
super();
super(参数列表);
3. super 访问父类方法示例
java
class Father {
public void show() {
System.out.println("这是父类的 show 方法");
}
}
class Son extends Father {
public void show() {
System.out.println("这是子类的 show 方法");
}
public void test() {
show();
super.show();
}
}
输出顺序为:
- 调用当前类重写后的方法
- 调用父类原始方法
4. this 与 super 的区别
这是常见考点。
this
- 表示当前对象
- 优先访问当前类成员
super
- 表示当前对象中的父类部分
- 用于访问父类成员
例如:
java
class Father {
int num = 10;
}
class Son extends Father {
int num = 20;
public void show() {
System.out.println(this.num); // 20
System.out.println(super.num); // 10
}
}
七、子类构造方法与父类构造方法
这是继承中的重点,也是很多人最容易出错的地方。
1. 子类对象创建时,一定会先调用父类构造方法
为什么?
因为子类对象中不仅包含子类自己的成员,还包含从父类继承来的那一部分。
既然父类部分也属于整个对象的一部分,那么创建子类对象时,就必须先把父类部分初始化好。
2. 默认会隐含调用 super()
看下面代码:
java
class Father {
public Father() {
System.out.println("父类无参构造方法");
}
}
class Son extends Father {
public Son() {
System.out.println("子类无参构造方法");
}
}
创建对象:
java
Son s = new Son();
输出结果:
text
父类无参构造方法
子类无参构造方法
原因是:
- 子类构造方法第一行默认隐藏着
super(); - 它会先调用父类无参构造
3. 如果父类没有无参构造怎么办
这时候子类必须显式调用父类有参构造,否则会报错。
示例:
java
class Father {
public Father(String name) {
System.out.println("父类有参构造:" + name);
}
}
class Son extends Father {
public Son() {
super("张三");
System.out.println("子类无参构造");
}
}
如果你不写 super("张三");,编译就会报错。
因为编译器默认想加 super();,但父类没有无参构造。
4. super(...) 必须写在构造方法第一行
这是语法规定。
例如下面写法是错误的:
java
class Son extends Father {
public Son() {
System.out.println("hello");
super();
}
}
因为 super() 必须放在第一行。
5. this(...) 与 super(...) 不能同时出现在同一构造方法第一行
因为构造方法第一行只能有一条语句来决定"先调谁"。
所以:
this(...)super(...)
二者只能二选一。
八、方法重写
继承讲到一定程度,就必须理解方法重写,因为多态几乎建立在重写之上。
1. 什么是方法重写
方法重写也叫覆盖,指的是:
子类中出现了和父类同名、同参数列表、同返回值类型的方法,用来重新定义父类原有行为。
例如:
java
class Animal {
public void speak() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("狗汪汪叫");
}
}
这里 Dog 中的 speak() 就重写了 Animal 中的 speak()。
2. 为什么要重写
因为父类提供的是"共性行为",而子类往往要提供"更具体的实现"。
例如:
- 动物都会叫
- 但狗的叫声和猫的叫声不同
因此父类只给出一个通用定义,子类再根据自己特点进行重写。
3. 方法重写的条件
通常可记为:
- 方法名相同
- 参数列表相同
- 返回值类型兼容
- 子类权限不能小于父类权限
- 子类重写方法不能抛出比父类更大的检查异常
在初学阶段,最核心的三个条件先记住:
- 同名
- 同参数
- 有继承关系
4. @Override 注解
重写时推荐写上:
java
@Override
作用:
- 告诉编译器这是一个重写方法
- 如果方法签名写错,编译器会帮你报错
这可以有效避免低级错误。
5. 方法重写示例
java
class Person {
public void work() {
System.out.println("人都需要工作");
}
}
class Teacher extends Person {
@Override
public void work() {
System.out.println("老师的工作是教书");
}
}
class Doctor extends Person {
@Override
public void work() {
System.out.println("医生的工作是治病救人");
}
}
这里 work() 在不同子类中有不同表现,这正是在为多态做准备。
九、方法重写与方法重载的区别
这是面试和考试中非常常见的比较题。
1. 方法重写
方法重写发生在:
- 父类和子类之间
特点:
- 方法名相同
- 参数列表相同
- 返回值类型兼容
目的:
- 子类重新定义父类行为
2. 方法重载
方法重载发生在:
- 同一个类中
- 也可以发生在父子类中,但本质判断仍看参数列表变化
特点:
- 方法名相同
- 参数列表不同
目的:
- 让同一个方法名适应不同参数情况
3. 对比总结
重写
- 看继承关系
- 看方法是否"完全对应"
重载
- 看参数列表是否不同
- 和返回值无关
4. 示例对比
重载
java
class MathTool {
public int add(int a, int b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
重写
java
class Animal {
public void move() {
System.out.println("动物会移动");
}
}
class Bird extends Animal {
@Override
public void move() {
System.out.println("鸟会飞");
}
}
十、什么是抽象类
1. 抽象类的基本概念
抽象类,顾名思义,就是"抽象出来的类"。
它往往用于描述一组子类共同具备的特征,但这个父类本身又不够具体,不适合直接创建对象。
例如:
- 动物都能吃东西,也都能发出声音
- 但不同动物发出声音的方式不一样
- "动物"这个类适合作为父类
- 但很多时候我们并不需要直接创建
Animal对象
这时就可以把 Animal 设计成抽象类。
2. 抽象类的语法
Java 中使用 abstract 关键字修饰类:
java
abstract class 类名 {
}
例如:
java
abstract class Animal {
}
这就表示 Animal 是一个抽象类。
3. 抽象类不能直接创建对象
这是抽象类最重要的特点之一。
例如:
java
abstract class Animal {
}
public class Test {
public static void main(String[] args) {
// Animal a = new Animal(); // 报错
}
}
原因很简单:
- 抽象类本身不够具体
- 它更像是一个"父类模板"
- 它的意义主要在于被继承,而不是直接实例化
4. 抽象类里可以有普通成员
很多同学一看到"抽象类",就误以为它里面只能写抽象方法。
其实不是。
抽象类中可以有:
- 普通成员变量
- 普通成员方法
- 构造方法
- 抽象方法
也就是说,抽象类并不是"什么都没有的空壳子",它完全可以把共性代码提前写好。
例如:
java
abstract class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + "正在吃东西");
}
}
这里虽然 Animal 是抽象类,但它照样可以有:
- 属性
name - 构造方法
- 普通方法
eat()
5. 什么是抽象方法
抽象方法就是:
只有方法声明,没有方法体的方法。
语法格式:
java
abstract 返回值类型 方法名(参数列表);
例如:
java
abstract class Animal {
public abstract void speak();
}
这里的 speak() 只有声明,没有具体实现。它的意思其实很明确:
- 所有动物都应该会"发出声音"
- 但具体怎么叫,父类不写死
- 交给各个子类自己去实现
6. 抽象类中可以有抽象方法,也可以没有
这是一个容易忽略的点。
情况一:抽象类中有抽象方法
这最常见,也最符合大家对抽象类的直觉。
情况二:抽象类中没有抽象方法
这也是合法的。此时把类声明为抽象类,主要目的是:
- 不让外界直接创建对象
- 强调这个类只是给子类继承使用
十一、抽象类和继承、多态之间是什么关系
1. 抽象类本质上还是父类
抽象类首先是一个类,所以它照样能被继承。
例如:
java
abstract class Animal {
public abstract void speak();
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("狗汪汪叫");
}
}
这里:
Animal是抽象父类Dog是它的子类
所以抽象类和继承并不冲突,反而常常是配套出现的。
2. 子类继承抽象类后,要么重写全部抽象方法,要么自己也声明为抽象类
这是语法规则。
例如:
java
abstract class Animal {
public abstract void speak();
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("狗汪汪叫");
}
}
这里 Dog 重写了 speak(),所以 Dog 可以是普通类。
但如果不重写:
java
abstract class Animal {
public abstract void speak();
}
abstract class Dog extends Animal {
}
那么 Dog 也必须继续声明为抽象类。
3. 抽象方法经常是在"强制子类重写"
普通父类也能写方法重写,但它偏向于"我先给你一个默认实现,你有需要再改"。抽象方法更像是在说:
这个行为对子类来说是必须有的,但父类这里不负责具体实现。
所以抽象类在设计上比普通父类更进一步,它不仅抽取共性,还能规定子类必须完成哪些行为。
4. 抽象类常常是多态最自然的父类型
看一个典型例子:
java
abstract class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + "正在吃东西");
}
public abstract void speak();
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(name + "汪汪叫");
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(name + "喵喵叫");
}
}
这时候就可以写:
java
Animal a1 = new Dog("旺财");
Animal a2 = new Cat("咪咪");
a1.speak();
a2.speak();
这里:
Animal作为抽象父类,负责抽取共性Dog、Cat负责给出具体实现- 最终通过父类引用体现多态
所以把继承、抽象类、多态连起来看,逻辑会很顺:
- 先有继承关系
- 再用抽象类把父类设计得更合理
- 最后用多态统一调用不同子类的行为
十二、什么是多态
1. 多态的基本概念
多态也是面向对象三大特征之一。所谓多态,可以直白地理解为:
同一种行为,由不同对象去完成时,会表现出不同的形态。
例如:
- 同样是调用
speak() - 狗对象会输出"汪汪"
- 猫对象会输出"喵喵"
这就是多态最直观的表现。
2. 生活中的类比
例如"按下开关"这个动作:
- 电灯响应后会发光
- 风扇响应后会转动
- 空调响应后会制冷
动作看起来一样,但具体结果不同。
这就是"同一操作,不同表现"。
3. Java 中的多态表现
在 Java 中,多态最常见的形式是:
父类引用指向子类对象。
例如:
java
Animal a = new Dog();
这里:
Animal是父类引用类型new Dog()是子类对象
这种写法就是多态的典型形式。
十三、多态成立的条件
想要形成多态,通常需要满足以下条件:
1. 要有继承或实现关系
例如:
Dog extends AnimalCat extends Animal
或者接口实现关系也可以形成多态,但本章先以类继承为主。
2. 要有方法重写
父类中定义通用方法,子类中进行重写。
如果没有重写,多态的动态效果就不明显。
3. 要有父类引用指向子类对象
例如:
java
Animal a = new Dog();
只有当引用类型和对象实际类型不一致时,多态特征才会真正体现出来。
4. 示例
java
class Animal {
public void speak() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("狗汪汪叫");
}
}
class Cat extends Animal {
@Override
public void speak() {
System.out.println("猫喵喵叫");
}
}
public class Test {
public static void main(String[] args) {
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.speak();
a2.speak();
}
}
输出:
text
狗汪汪叫
猫喵喵叫
这就是多态。
十四、父类引用指向子类对象怎么理解
这句话必须真正理解透。
1. 看起来很奇怪,实际上很合理
例如:
java
Animal a = new Dog();
很多初学者会问:
a到底是Animal还是Dog
正确理解是:
a的引用类型 是Animal- 它实际指向的对象类型是
Dog
也就是说:
"左边看类型,右边看对象。"
2. 为什么父类能接收子类对象
因为子类对象本质上也是一个父类对象。
例如"狗是动物",那么把狗当成动物来看待,是完全合理的。
这就像现实中:
- 你可以说"旺财是一条狗"
- 也可以说"旺财是一只动物"
第二种说法虽然更宽泛,但并没有错。
3. 这种设计有什么好处
好处非常大:
- 代码更通用
- 方法参数更灵活
- 更容易扩展新子类
例如你只写一个:
java
public void makeAnimalSpeak(Animal a) {
a.speak();
}
那么以后传入:
DogCatBird
都可以使用,而不需要为每个具体子类单独写方法。
十五、多态下访问成员变量和成员方法的规律
这是最经典、也是最容易混淆的考点。
1. 成员变量访问规律
先看代码:
java
class Father {
int num = 10;
}
class Son extends Father {
int num = 20;
}
public class Test {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.num);
}
}
输出是:
text
10
为什么?
因为:
成员变量没有多态性,访问看编译时左边引用类型。
也就是说:
f左边是Father- 所以访问
num时,看的是Father中定义的num
2. 成员方法访问规律
再看代码:
java
class Father {
public void show() {
System.out.println("父类 show");
}
}
class Son extends Father {
@Override
public void show() {
System.out.println("子类 show");
}
}
public class Test {
public static void main(String[] args) {
Father f = new Son();
f.show();
}
}
输出是:
text
子类 show
因为:
成员方法存在多态,运行时看右边对象真实类型。
3. 经典总结
这就是大家常说的:
编译看左边,运行看右边。
更准确地说:
- 编译阶段,看引用类型中是否有这个成员
- 运行阶段,方法调用看对象真实类型
但注意:
- 这个结论主要针对成员方法
- 成员变量不体现运行时重写机制
4. 口诀总结
可以这样记:
- 变量看左边
- 方法看右边
前提是这里讨论的是"多态访问"场景。
十六、多态的好处
1. 提高程序扩展性
这是多态最大的优势。
例如有一个动物表演程序:
java
public void perform(Animal a) {
a.speak();
}
以后新增:
DogCatTigerBird
只要它们都继承 Animal 并重写 speak(),原方法几乎不用改。
这就叫"面向抽象编程"。
2. 降低代码耦合度
如果一个方法只依赖父类或接口,而不依赖某个具体子类,那么这个方法就不容易因为新增子类而频繁改动。
3. 让代码更通用
例如:
java
public void feed(Animal a) { ... }
就比:
java
public void feedDog(Dog d) { ... }
public void feedCat(Cat c) { ... }
更通用。
4. 符合开闭原则思想
也就是:
- 对扩展开放
- 对修改关闭
新增功能时,尽量通过新增子类来扩展,而不是频繁修改旧代码。
十七、多态中的向上转型
1. 什么是向上转型
向上转型指的是:
把子类对象赋值给父类引用。
例如:
java
Animal a = new Dog();
这就是向上转型。
2. 为什么叫"向上"
因为在继承层次结构图中:
- 父类通常画在上面
- 子类画在下面
子类转为父类,就像往上走,所以叫向上转型。
3. 向上转型是否需要强制转换
不需要。
因为这是安全的。
子类本来就是父类的一种。
4. 向上转型后的特点
向上转型后,引用变量能访问的成员范围,取决于父类中定义的内容。
例如:
java
class Animal {
public void eat() {
System.out.println("动物吃");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("狗叫");
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog();
a.eat();
// a.bark(); // 报错
}
}
为什么 a.bark() 报错?
因为编译器只知道 a 是 Animal 类型引用。
而 Animal 类中没有 bark() 方法。
十八、多态中的向下转型
1. 什么是向下转型
向下转型指的是:
把父类引用重新转换为子类类型。
例如:
java
Animal a = new Dog();
Dog d = (Dog) a;
这样就能重新使用子类特有成员。
2. 为什么需要向下转型
因为向上转型后,父类引用无法直接访问子类特有方法。
如果确实需要调用子类独有行为,就要向下转型。
例如:
java
d.bark();
3. 向下转型需要强制类型转换
必须写:
java
(Dog) a
因为并不是所有 Animal 引用都一定指向 Dog 对象。
4. 向下转型有风险
例如:
java
Animal a = new Cat();
Dog d = (Dog) a;
这会在运行时抛出:
text
ClassCastException
因为 a 实际上指向的是 Cat 对象,不是 Dog 对象。
5. 安全转型要配合 instanceof
在向下转型前,一般先判断:
java
if (a instanceof Dog) {
Dog d = (Dog) a;
d.bark();
}
这样可以避免类型转换异常。
十九、instanceof 关键字详解
1. 作用
instanceof 用于判断某个对象是否属于某种类型。
语法:
java
对象引用 instanceof 类名
返回值是:
truefalse
2. 示例
java
Animal a = new Dog();
System.out.println(a instanceof Animal); // true
System.out.println(a instanceof Dog); // true
System.out.println(a instanceof Cat); // false
3. 为什么它很重要
因为多态下,引用类型和对象真实类型可能不一致。
在做向下转型前,必须尽量先确认真实类型,instanceof 就是最常用的工具。
二十、一个完整的继承、抽象类与多态案例
下面通过一个较完整的案例,把继承、抽象类、重写、多态、向上转型、向下转型串起来。
java
abstract class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + "正在吃东西");
}
public abstract void speak();
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(name + "汪汪叫");
}
public void guardHome() {
System.out.println(name + "正在看家");
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(name + "喵喵叫");
}
public void catchMouse() {
System.out.println(name + "正在抓老鼠");
}
}
public class Test {
public static void main(String[] args) {
Animal a1 = new Dog("旺财");
Animal a2 = new Cat("咪咪");
a1.eat();
a1.speak();
a2.eat();
a2.speak();
if (a1 instanceof Dog) {
Dog d = (Dog) a1;
d.guardHome();
}
if (a2 instanceof Cat) {
Cat c = (Cat) a2;
c.catchMouse();
}
}
}
运行结果会体现出:
- 父类构造被调用
- 子类重写方法生效
- 多态调用方法表现不同
- 向下转型后可以访问子类特有方法
这是一个非常典型、也非常适合练习的综合案例。
二十一、继承的优点与局限
1. 优点
继承的优点主要有:
- 提高代码复用性
- 减少重复定义
- 有利于建立清晰的类层次结构
- 为多态提供基础
2. 局限
继承不是越多越好。如果继承关系设计不合理,会导致:
- 类之间耦合过强
- 层次过深,理解困难
- 父类改动影响多个子类
3. 使用继承的原则
继承适合表达真正的"is-a"关系。
如果两个类之间只是"有一个""使用一个",就不一定适合继承,可能更适合组合。
例如:
- 人有一辆车
这里"车"不是"人"的一种,所以不能写成:
java
class Car extends Person
这显然不合理。
因此:
只有存在明确的"是一个"关系时,才考虑继承。
二十二、多态的应用思想
1. 面向父类编程
多态的核心思想之一,是尽量让代码依赖:
- 父类
- 抽象类
- 接口
而不是过度依赖某个具体实现类。
例如:
java
public void useAnimal(Animal a) {
a.speak();
}
这种写法比只接收 Dog 更有扩展性。
2. 统一调用,不同实现
在多态设计下,调用者不一定关心具体对象到底是哪一个子类,只要知道:
- 它具备某种共同行为
就可以统一调用。
3. 方便后续扩展
以后新增子类时:
- 不必大规模修改调用端
- 只需新增新的子类并重写对应行为
这正是优秀设计的体现。
二十三、开发中常见错误与误区
1. 误以为继承可以继承父类一切内容
错误。
构造方法不能被继承,私有成员也不能被子类直接访问。
2. 误以为子类对象创建时只执行子类构造
错误。
创建子类对象时,一定会先执行父类构造。
3. 误以为方法重写和重载是一回事
错误。
重写强调继承关系和相同参数列表;重载强调同名但参数不同。
4. 误以为多态下变量和方法都看右边
错误。多态下:
- 成员变量看左边
- 成员方法看右边
5. 误以为父类引用能直接调用子类特有方法
错误。
父类引用只能访问父类中声明过的成员。
想调用子类特有方法,通常要先向下转型。
6. 不加判断就强制向下转型
这很危险,可能导致 ClassCastException。
应优先使用 instanceof 进行判断。
7. 滥用继承
并不是所有类之间都适合继承。
只因为"代码长得像"就强行继承,往往会带来设计问题。
二十四、学习继承、抽象类与多态的建议
1. 先把继承关系画出来
学习时建议先把父类、子类关系画成树状图,例如:
text
Animal
├── Dog
├── Cat
└── Bird
这样更容易理解谁是谁的父类,谁能调用什么。
2. 多画内存和引用关系
例如:
java
Animal a = new Dog();
你要在脑中清楚:
- 左边是引用变量类型
- 右边是真实创建对象类型
这对理解多态非常关键。
3. 多写重写案例
建议练习下面这些主题:
- 动物叫
- 图形面积计算
- 员工工作内容
- 交通工具运行方式
这些都很适合练习继承和多态。
4. 遇到题目先问自己四个问题
第一问:谁是父类,谁是子类
先判断继承关系。
第二问:有没有方法重写
看子类是否重新定义了父类方法。
第三问:引用左边是什么类型
决定编译时能访问哪些成员。
第四问:右边真实对象是什么类型
决定运行时调用哪个重写方法。
只要把这四个问题理清,绝大多数继承与多态题目都能分析出来。
二十五、本章知识总结
1. 继承总结
继承表示子类获得父类中已有成员的能力,通过 extends 实现。
语法:
java
class 子类 extends 父类 {
}
继承的主要作用:
- 提高代码复用性
- 抽取共性
- 建立类层次结构
注意事项:
- Java 中类只能单继承
- 构造方法不能被继承
- 私有成员不能被子类直接访问
- 子类对象创建时会先调用父类构造方法
2. 抽象类总结
抽象类使用 abstract 修饰,主要用于:
- 提取共性
- 限制父类不能直接实例化
- 约束子类必须实现某些行为
特点:
- 抽象类不能直接
new - 抽象类中可以有普通成员,也可以有抽象方法
- 子类继承抽象类后,要么重写全部抽象方法,要么自己继续声明为抽象类
3. 重写总结
方法重写表示子类重新定义父类已有方法。
特点:
- 方法名相同
- 参数列表相同
- 有继承关系
作用:
- 实现子类特有行为
- 为多态提供基础
4. 多态总结
多态表示同一种行为在不同对象上有不同表现。
常见形式:
java
父类类型 引用名 = new 子类对象();
多态成立条件:
- 有继承关系
- 有方法重写
- 有父类引用指向子类对象
5. 成员访问规律总结
多态下:
- 成员变量看左边
- 成员方法看右边
6. 类型转换总结
- 向上转型:自动完成,安全
- 向下转型:强制转换,可能有风险
- 向下转型前建议使用
instanceof
二十六、结语
继承、抽象类与多态,是 Java 面向对象部分最关键的一组知识。
它们不是三个各讲各的零散点,而是一条很完整的线:先通过继承建立类之间的关系,再用抽象类把父类设计得更合理,最后通过多态把调用写得更灵活。
真正掌握这一章,不只是记住几个语法,而是要慢慢形成一种面向对象的写法:
- 共性的内容放到父类中
- 不够具体、但适合统一规范的父类,考虑写成抽象类
- 个性的内容放到子类中
- 调用时尽量面向父类或抽象类型编程
- 让程序在扩展新类型时更自然、更灵活
这一章难,不是因为代码多,而是因为脑中没有把"父类和子类""普通类和抽象类""引用类型和对象类型""编译时和运行时"这些概念分开。
一旦这些关系理顺,继承与多态其实非常有逻辑,也非常优雅。
因此学习本章时,建议不要只停留在"会写几个例子",而要反复练习去回答这些问题:
- 这个类为什么适合继承
- 这个父类是不是更适合写成抽象类
- 哪个方法应该重写
- 这个引用到底能访问什么
- 这个方法运行时到底调用谁
- 这里为什么需要转型
当这些问题你都能解释清楚时,说明你已经真正掌握了 Java 中的继承与多态。
二十七、附:常见语法速查表
1. 继承语法
java
class 子类 extends 父类 {
}
2. 抽象类与抽象方法
java
abstract class Animal {
public abstract void speak();
}
3. super 常见写法
java
super.成员变量;
super.成员方法();
super(参数列表);
4. 方法重写写法
java
@Override
public 返回值类型 方法名(参数列表) {
// 方法体
}
5. 多态写法
java
父类类型 引用名 = new 子类对象();
6. 向下转型写法
java
子类类型 变量名 = (子类类型) 父类引用;
7. instanceof 写法
java
if (对象引用 instanceof 子类类型) {
子类类型 变量名 = (子类类型) 对象引用;
}