Skip to content

第8章:面向对象(下)——继承与多态

第7章学会了造"设计图"(类)。这一章讲设计图之间的关系。

📌 代码说明:标注"可复制运行"的代码块包含完整的 main 方法。没标注的是概念示意片段。

你将学会
继承·多态·抽象类·接口
前置知识
第7章 类与对象
预计时间
40 分钟
需要准备
JDK + 编辑器
完成标志
用继承和接口设计可扩展的程序

一个故事开头

你的奶茶店生意越来越好了。现在菜单上不光有奶茶,还有水果茶、咖啡、冰沙。

每种饮品都有自己的做法,但也共享很多共同点:都有名字和价格,都能被制作和展示。

java
// 如果每个饮品都自己写一个类:
public class BubbleTea {
    String name;
    double price;
    void make() { /* 加珍珠、加茶... */ }
    void display() { /* 展示信息 */ }
}

public class FruitTea {
    String name;
    double price;
    void make() { /* 加水果、加茶... */ }
    void display() { /* 展示信息 */ }
}
// 名字、价格、display() 方法重复写了三遍

能不能把"共同的部分"提取出来,每种饮品只写自己"独特的部分"?**

能。这就是继承。


1. 继承——提取共同点

java
// 父类(基类):所有饮品共同的部分
public class Beverage {
    String name;
    double price;
    
    void display() {
        System.out.println(name + " — " + price + "元");
    }
}

// 子类(派生类):奶茶专有
public class MilkTea extends Beverage {
    String topping;  // 加料
    
    void addTopping(String t) {
        topping = t;
        System.out.println("加了 " + t);
    }
}

// 使用
MilkTea mt = new MilkTea();
mt.name = "波波奶茶";    // 继承自 Beverage
mt.price = 15.0;         // 继承自 Beverage
mt.display();            // 继承自 Beverage
mt.addTopping("珍珠");   // 自己的方法

extends = 继承。子类自动拥有父类的所有非 private 字段和方法。


2. 方法重写——子类改父类的做法

父类的方法可能不适合子类。子类可以**重写(Override)**它。

java
public class Beverage {
    String name;
    double price;
    
    void make() {
        System.out.println("制作 " + name + "...");
    }
}

public class MilkTea extends Beverage {
    @Override   // 注解:告诉编译器"我在重写父类方法"
    void make() {
        System.out.println("取杯 → 加茶底 → 加奶 → 加料 → 封口");
        System.out.println(name + " 做好了!");
    }
}

public class IceBlended extends Beverage {
    @Override
    void make() {
        System.out.println("取杯 → 加冰 → 加原料 → 打碎 → 装杯");
        System.out.println(name + " 做好了!");
    }
}

@Override 不是必须的,但强烈建议写——它能帮编译器检查你是不是真的在重写(写错方法名会立刻报错)。


3. super——喊爸爸

子类里可以用 super 访问父类的字段或方法。

java
public class Beverage {
    String name;
    
    Beverage(String name) {
        this.name = name;
    }
    
    void display() {
        System.out.println(name);
    }
}

public class MilkTea extends Beverage {
    String topping;
    
    MilkTea(String name, String topping) {
        super(name);      // 调父类的构造方法——必须在子类构造器的第一行
        this.topping = topping;
    }
    
    @Override
    void display() {
        super.display();  // 先执行父类的 display
        System.out.println("加料:" + topping);
    }
}

super = "父类"。 当子类重写了方法但还想用父类版本时,用 super.方法名()


4. 多态——同一件事,不同做法

java
Beverage b1 = new MilkTea("波波奶茶", "珍珠");
Beverage b2 = new IceBlended("芒果冰沙");

b1.make();  // 调的是 MilkTea 的 make()
b2.make();  // 调的是 IceBlended 的 make()

父类类型的变量,可以指向子类的对象。调用方法时,执行的是实际对象所属类的方法。

这就是"多态"——同一行代码 make(),不同的对象有不同的表现。

实际用途

java
public class OrderSystem {
    public static void processOrder(Beverage b) {
        // 不管传进来的是 MilkTea 还是 IceBlended,都能处理
        b.display();
        b.make();
        System.out.println("订单完成");
    }
    
    public static void main(String[] args) {
        processOrder(new MilkTea("波波奶茶", "珍珠"));
        processOrder(new IceBlended("芒果冰沙"));
    }
}

这就是多态的强大之处:写一个方法,能处理所有子类型的对象。新增一种饮品时,不用改 processOrder


5. 抽象类——只有定义,没有实现

有些类太"概念"了,不应该被直接创建对象。

"饮品"是个概念——你从菜单上点不了"一杯饮品",你只能点"波波奶茶"、"杨枝甘露"这种具体的东西。

java
// 抽象类——不能直接 new Beverage()
abstract class Beverage {
    String name;
    
    Beverage(String name) {
        this.name = name;
    }
    
    // 抽象方法——只有声明,没有方法体
    abstract void make();
    
    // 普通方法——子类可以直接用
    void display() {
        System.out.println(name);
    }
}

// 子类必须实现 make(),否则编译不通过
class MilkTea extends Beverage {
    MilkTea(String name) {
        super(name);
    }
    
    @Override
    void make() {
        System.out.println("制作奶茶");
    }
}

// Beverage b = new Beverage("test");  // ❌ 编译错误!抽象类不能 new

抽象类 = 半成品设计图——它规定了子类必须有什么方法(抽象方法),但实现由子类自己完成。


6. 接口——纯规定

如果说抽象类是"半成品",接口就是"合同"——它只规定"你必须有什么方法",完全不管你怎么实现。

java
// 接口——定义能力
interface Sellable {
    double getPrice();       // 必须实现
    void displayInfo();      // 必须实现
}

interface Discountable {
    double applyDiscount(double rate);  // 必须实现
}

// 类可以实现多个接口
class Merchandise implements Sellable, Discountable {
    String name;
    double price;
    
    Merchandise(String name, double price) {
        this.name = name;
        this.price = price;
    }
    
    @Override
    public double getPrice() {
        return price;
    }
    
    @Override
    public void displayInfo() {
        System.out.println(name + " — " + price + "元");
    }
    
    @Override
    public double applyDiscount(double rate) {
        return price * rate;
    }
}

接口 vs 抽象类

抽象类接口
能干什么定义通用代码 + 规定必须实现的方法只规定必须实现什么
能继承几个只能继承一个父类可以实现多个接口
有构造方法没有
字段可以有普通字段只能有常量(public static final
什么时候用多个类共享代码 + 行为只关心"有什么能力"

7. Java 8+ 的接口新特性

java
interface Greetable {
    void greet();  // 抽象方法
    
    // default 方法:接口里带有默认实现的普通方法
    default void sayGoodbye() {
        System.out.println("再见!");
    }
    
    // static 方法
    static void printWelcome() {
        System.out.println("欢迎光临!");
    }
}

class Customer implements Greetable {
    @Override
    public void greet() {
        System.out.println("你好!");
    }
    // sayGoodbye() 不用重写——直接用接口的默认版本
}

Customer c = new Customer();
c.greet();           // 你好!
c.sayGoodbye();      // 再见!(来自接口的 default 方法)
Greetable.printWelcome();  // 欢迎光临!(静态方法通过接口名调用)

8. 完整例子:奶茶店菜单系统

java
import java.util.ArrayList;

// 抽象类
abstract class Beverage {
    protected String name;
    protected double price;
    
    Beverage(String name, double price) {
        this.name = name;
        this.price = price;
    }
    
    abstract void make();
    
    void display() {
        System.out.println(name + " — " + price + "元");
    }
}

// 接口
interface Customizable {
    void addTopping(String topping);
    void adjustSweetness(int level);
}

// 具体子类
class MilkTea extends Beverage implements Customizable {
    private String topping = "";
    private int sweetness = 5;
    
    MilkTea(String name, double price) {
        super(name, price);
    }
    
    @Override
    void make() {
        System.out.println("制作" + name + ":取杯→加茶→加奶→加料→封口");
    }
    
    @Override
    public void addTopping(String topping) {
        this.topping = topping;
        System.out.println("已加料:" + topping);
    }
    
    @Override
    public void adjustSweetness(int level) {
        this.sweetness = level;
        System.out.println("甜度调整为:" + level + "分");
    }
}

class FruitTea extends Beverage {
    FruitTea(String name, double price) {
        super(name, price);
    }
    
    @Override
    void make() {
        System.out.println("制作" + name + ":取杯→加水果→加茶→加冰");
    }
}

// 主程序
public class MenuSystem {
    public static void main(String[] args) {
        ArrayList<Beverage> menu = new ArrayList<>();
        menu.add(new MilkTea("波波奶茶", 15.0));
        menu.add(new MilkTea("抹茶拿铁", 16.0));
        menu.add(new FruitTea("柠檬茶", 12.0));
        menu.add(new FruitTea("百香果绿", 14.0));
        
        // 多态遍历
        for (Beverage b : menu) {
            b.display();
            b.make();
            System.out.println("---");
        }
        
        // 向下转型——调子类专属方法
        MilkTea mt = new MilkTea("特制奶茶", 18.0);
        mt.addTopping("珍珠");     // 来自接口
        mt.adjustSweetness(3);     // 来自接口
        mt.make();
    }
}

本章小结

  1. 继承(extends)——子类自动拥有父类的非 private 成员,提取公共代码
  2. 方法重写(@Override)——子类覆盖父类的方法实现
  3. super——在子类里调用父类的构造方法或成员
  4. 多态——父类引用指向子类对象,运行时决定调哪个方法
  5. 抽象类(abstract)——不能 new,可以定义抽象方法来强迫子类实现
  6. 接口(interface)——纯合同,规定"有什么能力",一个类可以实现多个
  7. Java 8+ 接口增强——default 方法给接口加上默认实现

✅ 验收标准

完成本章后,你应该能:

  • [ ] 用 extends 实现继承
  • [ ] 用 @Override 重写父类方法
  • [ ] 用 super 调用父类构造器和方法
  • [ ] 理解多态——父类引用指向子类对象
  • [ ] 区分抽象类和接口的选择场景

📌 常见卡点

  • 子类构造器第一行必须调用父类构造器(显式 `super()` 或隐式调用无参)
  • 方法重写不能缩小访问权限——`public` 不能改成 `protected`
  • 静态方法不能重写——只能隐藏
  • 抽象类不能实例化

🔜 现在不需要理解

  • final 类和方法的作用
  • 接口的 private 方法(Java 9+)
  • 菱形继承问题——Java 通过接口避免
  • 密封类(Java 17+)


🧪 练习

1. 画继承图:画一个类继承图(在纸上或脑子里),包含 Animal(抽象类,有 void makeSound() 抽象方法)→ DogCat,它们分别实现 makeSound()

2. 写接口:定义一个 Playable 接口,包含 void play() 方法。让 Dog 实现它(输出"狗在玩球")。

3. 多态:写一个方法 static void playWith(Playable p),调用 p.play()。传一个 Dog 对象进去,看看调用的是哪个类的 play()


下一篇

第9章 字符串与常用工具类

用 ❤️ 构建 | Software Systems Atlas