×

继承学完了,里氏替换原则也懂了。现在问一个问题:如果父类中有一个方法,每个子类的实现都不一样,那父类写这个方法的代码有什么意义?

比如动物会「叫」——狗汪汪叫、猫喵喵叫、牛哞哞叫。你总不能写一个Animal.eat()的方法体,然后子类全给重写了吧?那父类的方法体就成摆设了,而且还容易犯一个错误——万一哪个子类忘了重写,就直接调了父类的空实现,Bug 静悄悄就来了。

Java 的设计者们当然想到了这个问题。他们提供了一个神奇的语法——抽象方法,它让父类只声明方法签名,不给实现体,强制子类去实现。如果一个类里包含抽象方法,那这个类自己也得声明为抽象类

抽象类 = 「部分完成」的类,它像一份半成品蓝图,把一些核心细节留给子类去完成。

一、先看感性的理解

我们来类比一个现实场景:你开了一个「厨师培训班」,培训流程是统一的——学员进教室、穿厨师服、老师讲理论、学员实操、品尝点评、下课离场。但是「实操」环节做什么菜呢?川菜班做麻婆豆腐、西点班做蛋糕、日料班做寿司……

你设计的培训流程里,「实操」环节只写了「学员实操并完成菜品」,但具体做啥——每个班不一样。你不可能替所有班的实操写好具体步骤。

在 Java 里,这就叫抽象方法:我知道要有这么个步骤,但具体怎么做,由子类决定。

二、语法入门

2.1 定义一个抽象类

// 抽象类用 abstract 关键字修饰
public abstract class Animal {

    // 普通属性
    protected String name;
    protected int age;

    // 普通方法——有方法体
    public void eat() {
        System.out.println(name + " 正在吃东西……");
    }

    // 抽象方法——没有方法体,只有声明
    public abstract void sound();

    // 抽象方法也可以有参数和返回值
    public abstract String getInfo();
}

注意几个关键点:

  1. 使用 abstract 关键字修饰类 → 这个类就是抽象类
  2. 抽象方法也只有 abstract 关键字,没有方法体(连花括号都没有,直接分号结尾)
  3. 抽象类可以同时包含普通方法和抽象方法——它不是”完全不可用的类”,而是”部分实现了的类”
  4. 抽象类也可以有构造器、成员变量、静态方法——普通类能有的它基本都有

2.2 子类实现抽象方法

// 子类必须实现父类所有的抽象方法,否则子类也要声明为抽象类
public class Dog extends Animal {

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void sound() {
        System.out.println(name + " 汪汪汪!");
    }

    @Override
    public String getInfo() {
        return "狗名:" + name + ",年龄:" + age + "岁";
    }
}

public class Cat extends Animal {

    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void sound() {
        System.out.println(name + " 喵喵喵~");
    }

    @Override
    public String getInfo() {
        return "猫名:" + name + ",年龄:" + age + "岁";
    }
}

2.3 使用抽象类和子类

public class Main {
    public static void main(String[] args) {
        // ❌ 抽象类不能直接 new
        // Animal animal = new Animal(); // 编译错误!

        // ✅ 但可以声明抽象类的引用,指向子类对象(向上转型)
        Animal dog = new Dog("旺财", 3);
        Animal cat = new Cat("咪咪", 2);

        dog.eat();    // 调的是普通方法(继承自Animal)
        dog.sound();  // 调的是 Dog 重写的 sound()
        System.out.println(dog.getInfo());

        cat.eat();
        cat.sound();
        System.out.println(cat.getInfo());
    }
}

输出:

旺财 正在吃东西……
旺财 汪汪汪!
狗名:旺财,年龄:3岁
咪咪 正在吃东西……
咪咪 喵喵喵~
猫名:咪咪,年龄:2岁

这样就完美了——Animal 定义了一个「合约」:所有动物都会叫,但具体怎么叫,每个动物自己决定。如果有人想新增一个 Pig 类,编译器会强制他实现 sound()getInfo() 方法,漏一个都编译不通过。合约强制遵守,这就叫设计保障。


三、抽象类的核心特性与注意事项

3.1 抽象类不能实例化

这是最基础但又最好理解的一条。new Animal() 直接编译报错。为什么?因为你创建一个”动物”对象,它叫不出来,没有意义——抽象类天生就是不完整的。

但反过来,上面你看到了:抽象类可以有构造器。这矛盾吗?不矛盾。 构造器是给子类用的——子类创建对象时,会通过 super() 调用父类的构造器来初始化父类中定义的属性。

public abstract class Animal {
    protected String name;
    protected int age;

    // 抽象类可以有构造器,但只能被子类调用
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public abstract void sound();
}

public class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age); // 调用父类构造器
    }

    @Override
    public void sound() {
        System.out.println(name + " 汪汪汪!");
    }
}

3.2 抽象类可以有成员变量和常量

抽象类可以定义实例变量,也可以定义常量 — 这些都可以被继承给子类使用。

public abstract class Shape {
    protected String color;         // 成员变量
    public static final double PI = 3.1415926535;  // 常量

    public Shape(String color) {
        this.color = color;
    }

    public abstract double area();     // 抽象方法
    public abstract double perimeter();

    // 普通方法:所有子类共用
    public void displayInfo() {
        System.out.println("颜色:" + color);
        System.out.println("面积:" + area());
        System.out.println("周长:" + perimeter());
    }
}

public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() {
        return PI * radius * radius; // 使用父类定义的 PI
    }

    @Override
    public double perimeter() {
        return 2 * PI * radius;
    }
}

public class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }

    @Override
    public double perimeter() {
        return 2 * (width + height);
    }
}

测试一下:

public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle("红色", 5.0);
        Shape rect = new Rectangle("蓝色", 4.0, 6.0);

        circle.displayInfo(); // 调普通方法
        System.out.println("---");
        rect.displayInfo();   // 多态:不同子类不同行为
    }
}

输出:

颜色:红色
面积:78.5398163375
周长:31.415926535

颜色:蓝色
面积:24.0
周长:20.0

3.3 抽象方法不能用 private、static、final 修饰

这是面试中经常挖坑的地方:

修饰符为什么不能和 abstract 一起用?
privateprivate 方法子类看不到,无法重写,但抽象方法的目标就是让子类实现,自相矛盾。
staticstatic 方法属于类,不能被子类重写(只能隐藏),抽象方法要求动态绑定,矛盾。
finalfinal 方法禁止子类重写,抽象方法强制子类实现,直接冲突。

一句话记:抽象方法天生就是要被重写的,所以和禁止重写的关键字(private/static/final)是死对头。

3.4 抽象类可以有静态方法和 main 方法

你没看错——抽象类也能有自己的 main() 方法,可以直接运行:

public abstract class Demo {
    public abstract void doSomething();

    public static void main(String[] args) {
        System.out.println("抽象类也能运行 main 方法!");
        // 但不能在这里创建自己的实例
    }
}

虽然你不能 new Demo(),但静态方法跟对象没关系,所以完全合法。这可以用来放一些工厂方法或者工具逻辑。

3.5 子类可以不被实现?

如果一个子类不想实现父类的所有抽象方法,那它自己也得是抽象类

public abstract class Animal {
    public abstract void sound();
    public abstract void move();
}

// Dog 只实现了 sound(),没实现 move() —— 它必须也声明为 abstract
public abstract class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("汪汪汪");
    }
    // move() 没实现,留给更具体的子类
}

// Husky 终于全部实现了
public class Husky extends Dog {
    @Override
    public void move() {
        System.out.println("哈士奇在满屋子跑……");
    }
}

这在大型继承体系里很常见——中间层抽象类负责实现一部分抽象方法,剩下的交给更具体的子类。逐层实现,逐渐具体化。


四、抽象类 vs 普通类:什么时候用抽象类?

对比维度抽象类普通类
实例化❌ 不能 new✅ 可以 new
抽象方法✅ 可以有❌ 不能有
成员变量✅ 可以有✅ 可以有
构造器✅ 可以有✅ 可以有
普通方法✅ 可以有✅ 可以有
静态方法/常量✅ 可以有✅ 可以有
设计目的定义公共属性和行为模板,部分行为不确定完整可用的类

什么时候用抽象类?记住这三点就够了:

  1. 多个子类有公共的属性和行为(成员变量 + 普通方法),可以提取到父类复用
  2. 部分行为每个子类各不相同,父类无法给出有意义的默认实现
  3. 想让父类不能被直接实例化,防止开发者误用

简单说:你发现不同类之间「有共同点,但某些行为就是不一样」,就把共同点抽象到父类,把不一样的地方声明成抽象方法。


五、实战案例:员工薪资计算系统

光讲理论不过瘾,咱们写一个实际练习。假设你在给一个公司写薪资系统:

  1. 全职员工(FullTimeEmployee):月薪固定,每月拿固定工资
  2. 兼职员工(PartTimeEmployee):按小时计薪,月底结算
  3. 实习生(Intern):按月领固定津贴,但金额比全职少

所有员工都有名字、工号和薪水计算方法,但计算方式不同——典型的抽象类场景。

5.1 抽象父类 Employee

public abstract class Employee {
    protected String name;
    protected String id;

    public Employee(String name, String id) {
        this.name = name;
        this.id = id;
    }

    // 抽象方法:计算月薪——每个员工类型不同
    public abstract double calculateMonthlySalary();

    // 普通方法:打印工资单(所有员工共用)
    public void printPaySlip() {
        System.out.println("=== 工资单 ===");
        System.out.println("姓名:" + name);
        System.out.println("工号:" + id);
        System.out.println("本月薪资:¥" + calculateMonthlySalary());
        System.out.println("==============");
    }
}

5.2 全职员工

public class FullTimeEmployee extends Employee {
    private double monthlySalary;

    public FullTimeEmployee(String name, String id, double monthlySalary) {
        super(name, id);
        this.monthlySalary = monthlySalary;
    }

    @Override
    public double calculateMonthlySalary() {
        // 全职员工简单的固定月薪
        return monthlySalary;
    }
}

5.3 兼职员工

public class PartTimeEmployee extends Employee {
    private double hourlyRate;    // 时薪
    private int hoursWorked;      // 本月工作小时数

    public PartTimeEmployee(String name, String id,
                            double hourlyRate, int hoursWorked) {
        super(name, id);
        this.hourlyRate = hourlyRate;
        this.hoursWorked = hoursWorked;
    }

    @Override
    public double calculateMonthlySalary() {
        return hourlyRate * hoursWorked;
    }
}

5.4 实习生

public class Intern extends Employee {
    private double stipend;  // 实习津贴

    public Intern(String name, String id, double stipend) {
        super(name, id);
        this.stipend = stipend;
    }

    @Override
    public double calculateMonthlySalary() {
        // 实习生只拿津贴
        return stipend;
    }
}

5.5 测试运行

public class PayrollSystem {
    public static void main(String[] args) {
        // 所有员工用 Employee 引用管理——多态
        Employee[] employees = new Employee[] {
            new FullTimeEmployee("张三", "FT001", 15000.0),
            new PartTimeEmployee("李四", "PT001", 50.0, 80),
            new Intern("王五", "IN001", 3000.0)
        };

        // 统一打印工资单
        for (Employee emp : employees) {
            emp.printPaySlip();
            System.out.println();
        }

        // 计算公司本月总薪资支出
        double total = 0;
        for (Employee emp : employees) {
            total += emp.calculateMonthlySalary();
        }
        System.out.println("公司本月薪资总支出:¥" + total);
    }
}

输出结果:

=== 工资单 ===
姓名:张三
工号:FT001
本月薪资:¥15000.0
==============

=== 工资单 ===
姓名:李四
工号:PT001
本月薪资:¥4000.0
==============

=== 工资单 ===
姓名:王五
工号:IN001
本月薪资:¥3000.0
==============

公司本月薪资总支出:¥22000.0

看到了吧——新增一种员工类型(比如改成Contractor外包员工),只需要再继承 Employee,实现一个 calculateMonthlySalary() 就行了。原有的循环管理代码一个字都不用改。开闭原则(对扩展开放,对修改关闭),抽象类帮我们做到了。


六、抽象类与继承的对比总结

到目前为止,你已经接触了继承和抽象类。做个对比:

对比维度普通继承抽象类
父类是否有方法体所有方法都有实现部分方法没有实现(抽象方法)
父类能否实例化✅ 可以❌ 不可以
子类必须实现的选择性重写(不强制)强制所有抽象方法
设计意图代码复用 + 层次关系定义行为规范 + 约束子类

有继承不一定需要抽象类,但有抽象方法必须是抽象类。当你写父类发现某个方法写不出通用的实现,或者写出来也没意义反正子类都要重写——那就该用抽象方法了。


七、面试中常见问题

整理几个面试高频题,帮你加深理解:

Q1:抽象类和普通类的区别?

最简单的答法:抽象类用 abstract 修饰、不能 new、可以有抽象方法——其他和普通类一样。抽象类的核心价值是定义行为规范,普通类的核心价值是直接使用

Q2:抽象方法能不能是 private 的?

不能。private 方法子类不可见,而抽象方法需要子类实现——private + abstract = 逻辑死循环

Q3:抽象类能不能没有抽象方法?

可以!你可以声明一个没有抽象方法的抽象类。虽然少见,但有时候你只是不想让别人 new 这个类,可以用 abstract class 把它卡住。比如 Java 的 HttpServlet 就是一个抽象类,它所有方法都有默认实现,但它不让你 new——必须继承重写。

Q4:抽象类能不能实现接口?

能!而且接口的抽象方法在抽象类里可以选择性实现——不实现的留给子类。这个我们在讲接口的时候会详细聊到。


结语

这篇我们讲了抽象类与抽象方法的核心概念和用法。一句话总结:抽象类是一份「半成品」模板,把确定的东西写好,把不确定的东西留给子类去填充。

但你有没有注意到,薪资系统里有个问题:实习生如果表现好,是不是可以转全职?Java 的类只能单继承——Intern一旦继承了 Employee,就不能再继承其他类了。如果「转全职」需要把 Intern 对象变成 FullTimeEmployee,那就尴尬了。

Java 的设计者用另一种语法解决了这个问题——接口(Interface)。下一篇咱们就聊聊接口,看它跟抽象类有什么区别、什么时候用接口、什么时候用抽象类。

代码写多了你自然会记住一句话:「抽象类是 is-a 关系,接口是 can-do 关系。」——下一篇,咱们来证明这句话。

我们下一篇见 🚀

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

作者

2248768396@qq.com

相关文章

20. 接口基础:语法、特性与抽象类对比

抽象类讲完了。你知道父类可以只声明方法签名,...

读出全部

19. 多态的应用:参数多态化、工厂模式与支付系统实战

上一篇我们拆解了多态的底层机制——动态绑定、...

读出全部

18. 多态的本质:动态绑定、向上转型与向下转型

抽象类学完了,你现在应该知道 「父类只声明、...

读出全部