抽象类与抽象方法 | JavaSE

抽象类与抽象方法

一、学习目标

学完本章,你应该能够:

  • 理解 abstract 关键字的基本含义。
  • 区分抽象类(Abstract Class)与抽象方法(Abstract Method)。
  • 正确使用 abstract class 定义抽象类。
  • 正确使用 abstract 定义只有方法声明、没有方法实现的抽象方法。
  • 理解为什么“有抽象方法的类必须是抽象类”。
  • 理解为什么“抽象类不一定必须包含抽象方法”。
  • 理解抽象类为什么不能直接使用 new 创建对象。
  • 理解抽象类仍然可以拥有成员变量、构造器、普通方法等成员。
  • 理解普通子类继承抽象类后为什么必须完成全部抽象方法。
  • 理解抽象子类为什么可以继续不实现某些抽象方法。
  • 能够利用抽象类统一规定子类必须具备的行为。
  • 能够使用抽象父类配合多态统一处理不同子类。
  • 独立完成 Animal → Dog / CatShape → 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 知识问答

  1. abstract 是什么?
  2. 什么是抽象类?
  3. 什么是抽象方法?
  4. 抽象方法为什么不能有普通方法体?
  5. 有抽象方法的类为什么必须声明为抽象类?
  6. 抽象类一定拥有抽象方法吗?
  7. 抽象类能否直接创建对象?
  8. 抽象类能否拥有构造器?
  9. 抽象类能否拥有成员变量?
  10. 抽象类能否拥有普通方法?
  11. 普通子类继承抽象类后必须做什么?
  12. 抽象子类为什么可以不实现全部抽象方法?
  13. 抽象类为什么适合作为父类型?
  14. 抽象类与多态是什么关系?
  15. abstract 类为什么不能同时 final?
  16. abstract 方法为什么不能 final?
  17. abstract 方法为什么不能 private?
  18. abstract 方法为什么不能 static?
  19. 父类什么时候适合把某个行为设计成抽象方法?
  20. 抽象方法和“空方法体”有什么本质区别?

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();
    }
}

回答:

  1. Animal 能否直接 new
  2. Animal 为什么仍然可以有构造器?
  3. 创建 Dog 时哪些构造器会执行?
  4. animal.eat() 调用哪里?
  5. animal.cry() 调用哪里?
  6. 这里体现了哪些多态知识?

阅读:

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 {

}

回答:

  1. B 为什么可以不实现 m2()
  2. C 当前能否编译?
  3. 如果不能,缺少哪个实现?
  4. 有哪两种修改思路?

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();
}

指出:

  1. 方法是什么类型?
  2. 类为什么不合法?
  3. 如何修复?

下面代码:

public abstract class Animal {

    public abstract void cry() {
        System.out.println("cry");
    }
}

回答:

  1. 抽象方法为什么不能这样写?
  2. 如果需要父类提供默认实现,应该怎么改?
  3. 如果需要子类强制实现,应该怎么改?

分析:

public abstract final class Animal {
}

解释:

  1. abstract 表达什么?
  2. final 表达什么?
  3. 为什么二者冲突?

7.5 综合训练

设计一个图形计算系统:

Shape
├── Circle
├── Rectangle
└── Triangle

要求:

Shape

color
area()

其中:

  • color 是公共状态;
  • 通过父类构造器初始化;
  • area() 必须是抽象方法。

三个具体子类:

  • 完成自己的构造器;
  • 实现不同面积公式。

然后:

public static double totalArea(
        Shape[] shapes
)

计算所有图形总面积。

要求:

  1. 不允许 new Shape()
  2. 不允许通过 instanceof 判断图形种类再计算面积。
  3. 必须利用方法重写和多态。
  4. 说明为什么 area() 特别适合设计成抽象方法。

7.6 本章验收

关闭资料,确认自己能够:

  • [ ] 手写 abstract class
  • [ ] 手写抽象方法。
  • [ ] 解释抽象方法为什么没有方法体。
  • [ ] 判断“抽象类一定有抽象方法”为什么错误。
  • [ ] 判断“有抽象方法的类必须是抽象类”。
  • [ ] 解释抽象类为什么不能直接 new。
  • [ ] 给抽象类编写字段、构造器和普通方法。
  • [ ] 正确编写普通子类并实现全部抽象方法。
  • [ ] 正确编写中间抽象子类。
  • [ ] 使用抽象父类型完成多态。
  • [ ] 从零完成 Shape / Circle / Rectangle。
  • [ ] 解释 abstract 与 final、private、static 的冲突。

如果你只是记住:

abstract = 抽象

却无法判断:

父类到底应该提供默认实现,还是应该只规定行为并强制子类实现,

那么还没有真正掌握抽象类。