讲解

有时父类本身没有完整实现的意义:「形状」怎么算面积?得看具体是圆还是方。抽象类(abstract class)就是为这种「半成品图纸」准备的:abstract class Shape 不能被 new(new Shape() 编译报错),只能被继承;里面的抽象方法 abstract double area(); 只有签名没有方法体,强迫每个子类必须给出自己的实现——少写一个都编译不过。抽象类也可以有普通的字段和已实现的方法,子类照常继承复用。

抽象类表达的是「是什么加上部分怎么做」:它在继承体系里卡位,把共性代码(字段、已实现方法)收拢在父类,把差异点(抽象方法)下放给子类。一个经典套路是「模板方法」:父类定义一个流程方法,里面按固定顺序调用若干抽象步骤,子类只需实现各个步骤,整个流程的骨架由父类把控——既复用了流程,又允许步骤各异。

没有抽象方法的类也可以声明成 abstract,效果仅仅是禁止实例化。判断要不要抽象类的经验法则:这个父类有没有「完整造出来」的语义?「动物」「形状」「支付方式」这种概念本身就造不出有意义的实例,就该是抽象的。另外抽象类和「全是抽象方法的类型」是有区别的——后者演进成了接口,下一章专门讲,两者的选择(单继承的抽象类 vs 多实现的接口)到那时再对比。

示例

抽象类定义抽象方法,子类各自实现:

abstract class Shape {
    protected String name;

    Shape(String name) {
        this.name = name;
    }

    abstract double area();

    public void describe() {
        System.out.println(name + " 的面积是 " + String.format("%.2f", area()));
    }
}

class Circle extends Shape {
    private final double radius;

    Circle(double radius) {
        super("圆形");
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}

class Square extends Shape {
    private final double side;

    Square(double side) {
        super("正方形");
        this.side = side;
    }

    @Override
    double area() {
        return side * side;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape c = new Circle(2);
        Shape s = new Square(3);
        c.describe();
        s.describe();
    }
}

模板方法模式:父类控制流程骨架,子类实现差异步骤:

abstract class Beverage {
    public final void prepare() {
        boilWater();
        brew();
        pourInCup();
    }

    private void boilWater() {
        System.out.println("烧水");
    }

    abstract void brew();

    private void pourInCup() {
        System.out.println("倒入杯中");
    }
}

class Tea extends Beverage {
    @Override
    void brew() {
        System.out.println("冲泡茶叶");
    }
}

class Coffee extends Beverage {
    @Override
    void brew() {
        System.out.println("滴滤咖啡粉");
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("--- 泡茶 ---");
        new Tea().prepare();
        System.out.println("--- 冲咖啡 ---");
        new Coffee().prepare();
    }
}

常见坑

  • 试图 new 一个抽象类:抽象类生来就是被继承的,new Shape() 编译报错。要实例就得 new 具体子类。
  • 子类漏实现抽象方法:普通类继承抽象类却没实现全部抽象方法,编译失败;除非子类自己也声明成 abstract,把义务继续往下传。
  • 抽象方法乱加修饰符:abstract 和 private、final 天然矛盾(一个要求子类实现,一个禁止子类碰),写在一起直接编译错误。
  • 抽象类与接口傻傻分不清:抽象类能存状态(字段)且走单继承,接口表达能力契约且可多实现。下一章详谈接口后再回看这条。

小结

抽象类不能实例化,抽象方法强制子类实现;模板方法让父类控流程、子类填步骤。下一节学习纯粹的「能力契约」:接口。