抽象类与抽象方法 | JavaSE
抽象类与抽象方法
一、学习目标
学完本章,你应该能够:
- 理解
abstract关键字的基本含义。 - 区分抽象类(Abstract Class)与抽象方法(Abstract Method)。
- 正确使用
abstract class定义抽象类。 - 正确使用
abstract定义只有方法声明、没有方法实现的抽象方法。 - 理解为什么“有抽象方法的类必须是抽象类”。
- 理解为什么“抽象类不一定必须包含抽象方法”。
- 理解抽象类为什么不能直接使用
new创建对象。 - 理解抽象类仍然可以拥有成员变量、构造器、普通方法等成员。
- 理解普通子类继承抽象类后为什么必须完成全部抽象方法。
- 理解抽象子类为什么可以继续不实现某些抽象方法。
- 能够利用抽象类统一规定子类必须具备的行为。
- 能够使用抽象父类配合多态统一处理不同子类。
- 独立完成
Animal → Dog / Cat、Shape → Circle / Rectangle等抽象类设计。
二、核心知识
2.1 从一个设计问题开始
前面已经学习继承。
假设存在:
Animal
├── Dog
├── Cat
└── Sheep
所有动物都会:
叫
所以可以在父类中定义:
public class Animal {
public void cry() {
System.out.println("动物会叫");
}
}
但是问题来了。
狗应该:
汪汪汪
猫应该:
喵喵喵
羊应该:
咩咩咩
父类 Animal 知道:
每一种具体动物都应该具备
cry()这个行为。
但是父类自己并不知道:
所有动物究竟应该统一怎么叫。
如果父类硬写:
public void cry() {
System.out.println("动物叫");
}
这个实现实际上非常模糊。
如果写:
public void cry() {
}
又只是留下一个空方法体,并没有真正表达:
子类必须给出自己的实现。
Java 为这种场景提供了:
abstract
2.2 abstract 是什么
abstract 是 Java 关键字。
当前阶段重点可以修饰:
类
方法
例如:
public abstract class Animal {
}
表示:
Animal是抽象类。
再例如:
public abstract void cry();
表示:
cry()是抽象方法。
2.3 什么是抽象类
使用:
abstract
修饰的类称为:
抽象类(Abstract Class)
基本语法:
访问修饰符 abstract class 类名 {
}
例如:
public abstract class Animal {
}
抽象类通常用于表达:
一个还不完整、不适合直接创建普通实例,但可以作为一组子类共同父类型的类。
例如:
Animal
这个概念很有价值。
我们可以知道动物:
有名字
有年龄
会吃东西
会发出声音
但现实业务中真正创建的往往是:
Dog
Cat
Sheep
Wolf
而不是一个什么具体特征都没有的:
“纯 Animal 对象”
这就是抽象类典型的设计意义。
2.4 什么是抽象方法
使用:
abstract
修饰的方法称为:
抽象方法(Abstract Method)
例如:
public abstract void cry();
完整类:
public abstract class Animal {
public abstract void cry();
}
注意:
public abstract void cry();
只有:
访问修饰符
abstract
返回值类型
方法名
参数列表
;
没有普通:
{
方法体
}
2.5 抽象方法为什么没有方法体
普通方法:
public void cry() {
System.out.println("汪汪");
}
已经回答:
cry()到底怎么执行?
而:
public abstract void cry();
只回答:
子类型必须存在一个
cry()行为。
但没有回答:
到底怎么叫?
具体实现交给:
Dog
Cat
Sheep
等子类。
所以抽象方法表达的是:
行为规范已经确定,实现细节尚未确定。
2.6 抽象类和抽象方法的基本语法
public abstract class Animal {
public abstract void cry();
}
可以拆成:
abstract class
↓
Animal 本身是抽象类
abstract void cry();
↓
cry 是抽象方法
2.7 有抽象方法的类必须是抽象类
错误:
public class Animal {
public abstract void cry();
}
这里:
Animal
是普通类。
却存在:
没有实现的方法 cry()
这是不允许的。
必须:
public abstract class Animal {
public abstract void cry();
}
因此:
一个类只要存在尚未实现的抽象方法,就必须声明为 abstract。
2.8 抽象类中一定要有抽象方法吗
不一定。
例如:
public abstract class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
这里:
没有任何 abstract method
仍然可以主动声明:
abstract class Animal
这是合法的。
为什么?
因为设计者可能只是希望表达:
Animal只作为父类型存在,不允许直接实例化。
所以必须记住:
有抽象方法
↓
这个类必须是抽象类
抽象类
↓
不一定必须有抽象方法
这两个方向不能倒着推。
2.9 抽象类能不能创建对象
不能:
Animal animal = new Animal();
如果:
Animal
是抽象类,上面代码编译失败。
为什么?
因为抽象类被设计成:
一个不完整或被视为不完整的类型。
例如:
public abstract class Animal {
public abstract void cry();
}
如果允许:
new Animal()
那么调用:
animal.cry();
到底应该执行什么?
根本没有具体实现。
因此 Java 从语言层面禁止:
直接实例化抽象类
2.10 抽象类不能 new,是不是抽象类没有构造器
不是。
这是特别重要的区别。
抽象类:
public abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
完全可以拥有:
public Animal(String name)
构造器。
只是不能直接:
new Animal("动物");
而可以由子类构造过程调用:
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
}
再:
Dog dog = new Dog("旺财");
创建 Dog 对象时:
Dog 构造器
↓
super(name)
↓
Animal 构造器
仍然正常执行。
所以:
抽象类不能直接实例化,不等于抽象类没有构造器。
2.11 抽象类可以拥有哪些成员
原教学资料明确强调:
普通类有的成员,抽象类基本也可以拥有。
例如:
public abstract class Animal {
private String name;
public static String category =
"动物";
public Animal() {
}
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void eat() {
System.out.println(
name + "正在吃东西"
);
}
public abstract void cry();
}
这里同时存在:
成员变量
static 成员
构造器
普通实例方法
抽象方法
因此抽象类并不是:
“只能写 abstract 方法的特殊文件”。
它仍然是真正的类。
2.12 普通子类继承抽象类后怎么办
父类:
public abstract class Animal {
public abstract void cry();
}
子类:
public class Dog extends Animal {
}
这段代码不能通过编译。
为什么?
因为:
Dog
是普通、可实例化的类。
如果允许:
Dog dog = new Dog();
那么:
dog.cry();
仍然没有具体实现。
所以普通子类必须:
@Override
public void cry() {
System.out.println("汪汪汪");
}
完整:
public class Dog extends Animal {
@Override
public void cry() {
System.out.println(
"狗是汪汪汪地叫"
);
}
}
2.13 普通子类必须实现全部抽象方法
父类:
public abstract class Animal {
public abstract void cry();
public abstract void move();
}
子类只实现:
public class Dog extends Animal {
@Override
public void cry() {
System.out.println("汪汪");
}
}
仍然不行。
因为:
move()
还没有实现。
普通子类必须把继承体系中自己需要负责的抽象方法全部具体化:
public class Dog extends Animal {
@Override
public void cry() {
System.out.println("汪汪");
}
@Override
public void move() {
System.out.println("狗奔跑");
}
}
2.14 子类也可以继续是抽象类
父类:
public abstract class Animal {
public abstract void cry();
public abstract void move();
}
中间子类:
public abstract class Mammal
extends Animal {
@Override
public void move() {
System.out.println(
"哺乳动物移动"
);
}
}
这里只实现:
move()
没有实现:
cry()
为什么仍然合法?
因为:
Mammal
自己也是:
abstract
意味着:
Mammal 本身仍然可以继续保持不完整,把剩余抽象行为交给更具体的子类。
例如:
public class Dog extends Mammal {
@Override
public void cry() {
System.out.println("汪汪");
}
}
所以规则真正是:
一个非抽象子类必须为尚未实现的抽象方法提供具体实现。
2.15 抽象类最重要的应用场景
假设:
父类知道:
所有子类都应该完成某个行为
但是:
不同子类完成方式不同
例如:
Animal
↓
所有动物都会 cry()
但具体叫法不同
于是父类:
public abstract class Animal {
public abstract void cry();
}
子类:
public class Cat extends Animal {
@Override
public void cry() {
System.out.println("喵喵喵");
}
}
public class Dog extends Animal {
@Override
public void cry() {
System.out.println("汪汪汪");
}
}
这正是原课程总结的:
父类确定行为,具体实现交给子类。
2.16 抽象类与多态
抽象类虽然不能:
new Animal()
但完全可以作为:
变量类型
参数类型
返回值类型
例如:
Animal animal =
new Dog();
完全合法。
因为:
不能 new Animal
和:
不能使用 Animal 作为引用类型
根本不是一回事。
2.17 使用抽象类实现多态
public static void makeCry(
Animal animal
) {
animal.cry();
}
调用:
makeCry(
new Dog()
);
makeCry(
new Cat()
);
方法内部:
animal.cry();
统一调用。
实际运行:
Dog
↓
Dog.cry()
Cat
↓
Cat.cry()
这就是:
抽象父类
+
抽象方法
+
子类重写
+
多态
组合起来的典型效果。
2.18 抽象类比普通父类多表达了什么
普通父类:
public class Animal {
public void cry() {
}
}
对子类只表达:
这里有一个默认的 cry 方法。
子类完全可以不重写。
而:
public abstract class Animal {
public abstract void cry();
}
表达:
cry()是这个类型体系必须具备的行为,但是 Animal 不提供通用实现;具体非抽象子类必须解决它。
这是一种更强的设计约束。
2.19 抽象方法不能有普通方法体
错误:
public abstract void cry() {
System.out.println("hello");
}
因为:
abstract method
本身意味着:
方法声明存在,但这里没有提供实现。
所以必须:
public abstract void cry();
以:
;
结束。
2.20 abstract 和 final 为什么矛盾
假设:
public abstract final class Animal {
}
这两个关键字的设计意图直接冲突。
abstract class
↓
类还不完整
通常需要子类继续实现/扩展
final class
↓
禁止任何子类继承
于是:
需要子类
+
禁止子类
无法同时成立。
因此:
abstract final class
是不合法的组合。
2.21 abstract 方法为什么不能 final
同理:
public abstract final void cry();
也矛盾。
因为:
abstract method
↓
等待子类提供实现
final method
↓
不允许子类重写
因此不能同时存在。
2.22 abstract 方法为什么不能 private
假设:
private abstract void cry();
如果方法:
private
意味着:
子类无法继承访问它作为一个可重写的成员。
但:
abstract
又要求:
后续子类需要提供实现。
两种要求互相冲突。
所以抽象方法不能声明:
private
2.23 abstract 方法为什么不能 static
实例抽象方法解决:
由不同实际子类对象提供不同实现。
而:
static
方法属于:
类
不参加普通实例方法动态重写机制。
所以:
public static abstract void test();
不允许。
三、使用方法
3.1 Animal 案例
抽象父类:
public abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void eat() {
System.out.println(
name + "正在吃东西"
);
}
public abstract void cry();
}
Dog:
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void cry() {
System.out.println(
getName() + ":汪汪汪"
);
}
}
Cat:
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void cry() {
System.out.println(
getName() + ":喵喵喵"
);
}
}
测试:
public class Test {
public static void main(String[] args) {
Animal a1 =
new Dog("旺财");
Animal a2 =
new Cat("咪咪");
a1.eat();
a1.cry();
a2.eat();
a2.cry();
}
}
这里:
eat()
已经存在通用实现,因此直接放在父类。
而:
cry()
不同动物差异很大,所以设计成抽象方法。
3.2 Shape 面积案例
很多图形都存在:
计算面积
这个行为。
但公式不同:
圆:
πr²
矩形:
width × height
因此:
public abstract class Shape {
public abstract double area();
}
Circle:
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI
* radius
* radius;
}
}
Rectangle:
public class Rectangle
extends Shape {
private double width;
private double height;
public Rectangle(
double width,
double height
) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
3.3 通过 Shape 父类型统一处理
public static void printArea(
Shape shape
) {
System.out.println(
shape.area()
);
}
调用:
printArea(
new Circle(3)
);
printArea(
new Rectangle(4, 5)
);
这里:
printArea()
完全不关心:
到底是 Circle
还是 Rectangle
只关心:
它是一个 Shape,并且能够计算
area()。
3.4 使用数组统一保存抽象类型对象
Shape[] shapes = {
new Circle(3),
new Rectangle(4, 5),
new Circle(10)
};
遍历:
for (Shape shape : shapes) {
System.out.println(
shape.area()
);
}
这再次体现:
抽象父类型
+
多态
可以统一管理不同具体实现。
四、原理与进阶
4.1 抽象类表达“共性 + 不完整性”
普通继承:
Animal
├── 通用字段
├── 通用方法
└── 子类继承
抽象类进一步表达:
Animal
├── 已经确定的公共状态
├── 已经确定的公共行为
└── 必须存在、但这里无法统一实现的行为
所以抽象类非常适合:
既存在公共代码,又存在必须交由不同子类实现的差异行为。
4.2 抽象方法是一种约束
父类:
public abstract double area();
实际上是在对所有具体 Shape 子类说:
如果你想成为一个可以直接实例化的 Shape,那么你必须给出面积计算规则。
所以抽象方法并不是:
“因为懒得写代码,所以空着”。
而是在建立:
类型契约。
4.3 抽象类为什么适合作为父类型
调用方:
public void print(Shape shape)
只依赖:
Shape
不依赖:
Circle
Rectangle
Triangle
以后新增:
public class Triangle
extends Shape {
...
}
只要完成:
area()
原有:
print(Shape shape)
通常不需要修改。
这与上一章多态强调的:
依赖稳定父类型
完全衔接。
4.4 抽象类不能实例化,但构造器仍然非常重要
例如:
public abstract class Shape {
private String color;
public Shape(String color) {
this.color = color;
}
}
Circle:
public Circle(
String color,
double radius
) {
super(color);
this.radius = radius;
}
这里父类构造器承担:
初始化所有 Shape 共有状态。
因此:
abstract
绝不意味着:
“这个类不参与对象初始化”
它只是不能成为:
new 的最终具体类型
4.5 抽象类与普通类的关系
抽象类仍然支持已经学过的大多数类机制:
封装
构造器
this / super
继承
方法重写
多态
static
final 成员
成员变量
普通实例方法
所以抽象类不是一个全新的对象体系。
它是在:
类与继承体系之上增加“未完成行为”这一表达能力。
4.6 抽象类并不是“越抽象越高级”
错误设计可能是:
A
↑
AbstractB
↑
AbstractC
↑
AbstractD
↑
ConcreteE
为了使用 abstract 而创建大量没有明确意义的层次,会增加:
- 理解成本;
- 继承层级;
- 维护复杂度。
因此只有当确实存在:
合理父子关系
+
共同状态/行为
+
部分行为必须由子类实现
时,抽象类才非常自然。
五、实践应用
5.1 文件导出器
假设:
CSVExporter
PDFExporter
ExcelExporter
都属于:
Exporter
而导出方式不同。
可以:
public abstract class Exporter {
public abstract void export();
}
不同子类分别实现。
5.2 支付体系
例如:
Payment
├── AlipayPayment
├── WeChatPayment
└── BankCardPayment
如果当前设计中它们拥有共同状态和公共实现,也可以考虑抽象父类。
但后面学习:
接口
后,还会看到另一种表达共同能力的方式。
所以这里暂时不做:
抽象类 vs 接口
完整比较。
这部分留给后续接口章节。
5.3 游戏角色体系
GameCharacter
├── Warrior
├── Mage
└── Archer
父类可能统一:
name
hp
level
move()
而:
attack()
不同职业规则明显不同。
就可能:
public abstract void attack();
让具体角色实现。
六、常见问题
6.1 abstract 可以修饰什么?
本章重点:
类
成员方法
6.2 什么是抽象类?
使用:
abstract
修饰,并被认为不完整、不能直接实例化的类。
6.3 什么是抽象方法?
只有方法声明、不提供具体方法实现的 abstract 方法。
例如:
public abstract void test();
6.4 抽象方法可以写方法体吗?
不可以。
普通 Java 抽象方法使用:
;
结束。
6.5 抽象类一定有抽象方法吗?
不一定。
6.6 有抽象方法的普通类合法吗?
不合法。
类必须声明:
abstract
6.7 抽象类为什么不能 new?
因为它被语言定义为不完整类型,不能作为类实例创建表达式的直接实例类型。
6.8 抽象类有构造器吗?
可以有。
构造器会在创建具体子类对象时通过构造链执行。
6.9 抽象类可以有普通方法吗?
可以。
6.10 抽象类可以有成员变量吗?
可以。
6.11 子类继承抽象类后一定要实现抽象方法吗?
如果子类本身是:
普通非抽象类
那么必须完成继承体系中尚未实现的抽象方法。
如果子类仍声明:
abstract
则可以继续保留未实现行为。
6.12 抽象方法可以 private 吗?
不可以。
因为 private 方法无法作为供子类实现的可重写成员。
6.13 抽象方法可以 static 吗?
不可以。
static 方法不参加普通实例方法重写机制。
6.14 abstract 和 final 可以一起修饰类吗?
不可以。
一个要求继续通过子类完善,一个禁止继承,语义冲突。
6.15 抽象类为什么有利于多态?
因为它能够:
统一父类型
+
规定共同方法
+
强制具体子类给出不同实现
调用者即可通过父类型统一调用。
七、练习与验收
7.1 知识问答
abstract是什么?- 什么是抽象类?
- 什么是抽象方法?
- 抽象方法为什么不能有普通方法体?
- 有抽象方法的类为什么必须声明为抽象类?
- 抽象类一定拥有抽象方法吗?
- 抽象类能否直接创建对象?
- 抽象类能否拥有构造器?
- 抽象类能否拥有成员变量?
- 抽象类能否拥有普通方法?
- 普通子类继承抽象类后必须做什么?
- 抽象子类为什么可以不实现全部抽象方法?
- 抽象类为什么适合作为父类型?
- 抽象类与多态是什么关系?
- abstract 类为什么不能同时 final?
- abstract 方法为什么不能 final?
- abstract 方法为什么不能 private?
- abstract 方法为什么不能 static?
- 父类什么时候适合把某个行为设计成抽象方法?
- 抽象方法和“空方法体”有什么本质区别?
7.2 代码阅读
不运行:
abstract class Animal {
public Animal() {
System.out.println(
"Animal constructor"
);
}
public void eat() {
System.out.println("eat");
}
public abstract void cry();
}
class Dog extends Animal {
public Dog() {
System.out.println(
"Dog constructor"
);
}
@Override
public void cry() {
System.out.println("wang");
}
}
public class Test {
public static void main(String[] args) {
Animal animal =
new Dog();
animal.eat();
animal.cry();
}
}
回答:
Animal能否直接new?Animal为什么仍然可以有构造器?- 创建 Dog 时哪些构造器会执行?
animal.eat()调用哪里?animal.cry()调用哪里?- 这里体现了哪些多态知识?
阅读:
abstract class A {
public abstract void m1();
public abstract void m2();
}
abstract class B extends A {
@Override
public void m1() {
System.out.println("m1");
}
}
class C extends B {
}
回答:
B为什么可以不实现m2()?C当前能否编译?- 如果不能,缺少哪个实现?
- 有哪两种修改思路?
7.3 手写代码
任务一:Animal
设计:
Animal
├── Dog
└── Cat
要求:
Animal:
name
eat()
cry()
其中:
eat()提供公共实现;cry()为抽象方法;- Dog、Cat 分别实现不同叫声;
- 使用
Animal类型变量调用。
任务二:Shape
设计:
Shape
├── Circle
└── Rectangle
要求:
public abstract double area();
Circle:
radius
Rectangle:
width
height
然后:
public static void printArea(
Shape shape
)
统一输出面积。
任务三:Shape 数组
创建:
Shape[] shapes
至少保存:
2 个 Circle
2 个 Rectangle
循环:
shape.area()
计算全部面积。
禁止:
if shape instanceof Circle
因为 area() 本来就是公共抽象行为,应通过多态完成。
7.4 Debug
下面代码为什么错误?
public class Animal {
public abstract void cry();
}
指出:
- 方法是什么类型?
- 类为什么不合法?
- 如何修复?
下面代码:
public abstract class Animal {
public abstract void cry() {
System.out.println("cry");
}
}
回答:
- 抽象方法为什么不能这样写?
- 如果需要父类提供默认实现,应该怎么改?
- 如果需要子类强制实现,应该怎么改?
分析:
public abstract final class Animal {
}
解释:
abstract表达什么?final表达什么?- 为什么二者冲突?
7.5 综合训练
设计一个图形计算系统:
Shape
├── Circle
├── Rectangle
└── Triangle
要求:
Shape:
color
area()
其中:
color是公共状态;- 通过父类构造器初始化;
area()必须是抽象方法。
三个具体子类:
- 完成自己的构造器;
- 实现不同面积公式。
然后:
public static double totalArea(
Shape[] shapes
)
计算所有图形总面积。
要求:
- 不允许
new Shape()。 - 不允许通过
instanceof判断图形种类再计算面积。 - 必须利用方法重写和多态。
- 说明为什么
area()特别适合设计成抽象方法。
7.6 本章验收
关闭资料,确认自己能够:
- [ ] 手写
abstract class。 - [ ] 手写抽象方法。
- [ ] 解释抽象方法为什么没有方法体。
- [ ] 判断“抽象类一定有抽象方法”为什么错误。
- [ ] 判断“有抽象方法的类必须是抽象类”。
- [ ] 解释抽象类为什么不能直接 new。
- [ ] 给抽象类编写字段、构造器和普通方法。
- [ ] 正确编写普通子类并实现全部抽象方法。
- [ ] 正确编写中间抽象子类。
- [ ] 使用抽象父类型完成多态。
- [ ] 从零完成 Shape / Circle / Rectangle。
- [ ] 解释 abstract 与 final、private、static 的冲突。
如果你只是记住:
abstract = 抽象
却无法判断:
父类到底应该提供默认实现,还是应该只规定行为并强制子类实现,
那么还没有真正掌握抽象类。