☕ 第四章 面向对象(下)

4.3 抽象类和接口

📄 一句话概括

抽象类和接口是两种"立规矩"的工具:抽象类是半成品图纸(公共实现 + 强制子类补的空格子),接口是纯能力合同(只列要求不给实现)。它们把"父类说了算"升级成"父类只管要求,实现归子类"——多态的前菜。


1. 先看继承的一个尴尬:父类的方法没法写

4.1 的 Animal 里有个 sound(),当时没写。现在试着写写看:

class Animal {
    void sound() {
        ???            // 狗汪汪、猫喵喵、鸟啾啾……写哪个都错
    }
}

父类里根本没有"正确答案"——叫声是每个子类自己的事。两种凑合写法都有毛病:

根本矛盾:我们想要的是——"动物必须会叫"这条规矩留在父类,但怎么叫必须由子类自己写,而且谁漏写就让谁的代码编译不过。

这就是 abstract(抽象)登场的地方。


2. 抽象类:半成品图纸

abstract class Animal {                    // ① 类加 abstract
    String name;
    int age;

    void breathe() {                       // ② 普通方法照常实现(白得的部分)
        System.out.println(name + "在呼吸");
    }

    abstract void sound();                 // ③ 抽象方法:只有签名,没有方法体
}

抽象方法长这样:abstract void sound();——直接分号结尾,连 { } 都没有。意思是:"这条规矩必须兑现,怎么兑现我不管。"

四条硬规矩(编译器盯着)

# 规矩 原因
1 类里有抽象方法,类必须声明为 abstract 有没兑现的规矩,就不是完整图纸
2 抽象类不能 new 半成品造不出能用的实物——new Animal() 编译报错
3 子类必须重写全部抽象方法,否则子类自己也得是 abstract 兑现不完就接着当下一个半成品
4 抽象类可以没有抽象方法(就是不想被 new 而已) 有时锁 new 本身就是目的

规矩 3 是本节的精髓:编译器代替你检查"子类有没有兑现规矩"——之前"忘了重写不报错"的隐患,被连根拔掉。

class Dog extends Animal {
    void sound() {                         // 不写它,Dog 直接编译报错
        System.out.println(name + "汪汪叫");
    }
}
📝 抽象类也有构造方法

不能 new ≠ 没有构造。子类创建时依然会先执行父类构造(给公共属性赋值)——"构造方法不被继承但会被调用",4.1 埋的伏笔,下节 super 收网。


3. 接口:纯能力合同

抽象类还是有"私货":属性、实现好的方法。接口干脆全扔掉,只留合同:

interface Flyable {                // ① 关键字是 interface,不是 class
    void fly();                    // ② 方法自动是 public abstract,不用写
    int MAX_HEIGHT = 10000;        // ③ 属性自动是 public static final 常量
}

class Bird extends Animal implements Flyable {   // ④ implements 兑现合同
    void sound() { System.out.println(name + "啾啾叫"); }

    public void fly() {            // ⑤ 必须写 public,少写编译报错
        System.out.println(name + "飞到 " + MAX_HEIGHT + " 米");
    }
}

接口的关键词都是"自动":

接口最大的本事:弥补单继承

4.1 说过 Java 单继承——爹只能一个。但合同可以签很多份:

class Bird extends Animal implements Flyable, Singable {
    // 一个爹 + N 份能力合同,全都能要
}

为什么接口能多签?因为接口只有规矩没有实现,不存在"两个爹打架"——规矩冲突时反正要你自己兑现,编译器只管检查你交没交作业。4.1 说的"Java 用单继承 + 接口绕开多继承的坑",就是这。

注意实现接口时的一个细节:实现方法必须写 public——接口方法默认是 public,你实现时权限只能放宽不能收紧,不写就是默认权限,直接报错。


4. 抽象类 vs 接口:一张表定选择

抽象类 接口
关键字 abstract class interface
挂上去 extends(只能一个) implements(可以多个)
里面装什么 什么都行:属性、实现方法、抽象方法 常量 + 抽象方法(纯合同)
有构造方法吗 有(给子类用) 没有
回答的问题 你是什么(is-a) 你会什么(can-do)
典型例子 Animal:动物家族的爹 Flyable:会飞的能力

选择口诀一句话:

共性(家族血缘 + 公共实现)→ 抽象类
能力(跨家族的通用本事)→ 接口

拿现实检验:鸟和飞机——"飞机是一种鸟"?荒谬,血缘不同。但"都会飞"是共同能力 → 飞的能力就该做成 Flyable 接口,鸟和飞机各自 implements。而狗、猫、鸟的公共属性和呼吸 → 走 Animal 抽象类。is-a 用抽象类,can-do 用接口,面试和实战都是这句。


5. 和老知识的三处接头(复习锚点)


6. 动手练习(保姆式,接着 IDEA04 项目写)

📝 一次一步

① 打开 IDEA04,src 右键 → New → Java Class → 输入 Animal,替换成第 2 节的抽象类版本(原 class 前加 abstract,bark 换成抽象 sound) ② src 右键 → New → Java Class → 输入 Flyable——注意列表里选 Interface(默认选的是 Class,别选错) ③ 把 Dog 改成重写 sound():void sound() { System.out.println(name + "汪汪叫"); } ④ 新建 Bird 类抄第 3 节的 Bird(extends Animal implements Flyable) ⑤ 新建 Test01 跑通:

public class Test01 {
    public static void main(String[] args) {
        Animal a = new Dog();      // 父类钥匙指子类车——先混脸熟,下节讲透
        a.name = "旺财";
        a.breathe();
        a.sound();                 // 旺财汪汪叫

        Bird b = new Bird();
        b.name = "翠花";
        b.sound();                 // 翠花啾啾叫
        b.fly();                   // 翠花飞到 10000 米
    }
}

成功标准:四行输出一字不差。

三个 30 秒实验(每个都值得做):

  1. new 抽象类:把 new Dog() 临时改成 new Animal()——看"抽象类不能实例化"的报错长什么样;
  2. 漏兑现合同:把 Bird 的 fly() 整个删掉——看编译器怎么拦"没兑现合同"的类(红线 + 报错里出现 implements 字样);
  3. 少写 public:把 public void fly() 的 public 去掉——看"权限不能比接口更严格"的报错。

7. 常见误区

⚠️ 误区 1:抽象类里必须至少有一个抽象方法

不用。没有任何抽象方法的抽象类完全合法——目的就是不许别人 new,只许当爹。规矩 4。

⚠️ 误区 2:抽象类不能有构造方法

有!而且必须给子类调用。不能 new 和没有构造是两回事——构造是给"儿子出生时先给爹签到"用的。

⚠️ 误区 3:接口就是一种特殊的类

接口不是类,是独立的类型。它没有构造方法、不能 new、成员全是常量——用 class 的眼光看接口哪哪都别扭。

⚠️ 误区 4:实现接口方法时可以不写 public

不行。接口方法默认 public,实现时权限只能放宽不能收紧——不写就是默认权限,比 public 窄,编译报错。动手实验 3 现场验证。

⚠️ 误区 5:有了继承就不需要接口了(或反过来)

两者分工不同:抽象类管"家族共性 + 公共实现",接口管"跨家族能力"。成熟的设计往往是抽象类打地基、接口装能力——Bird extends Animal implements Flyable 就是标准姿势。


8. 自测题(先自己答,再展开)

❓ Q1:为什么父类的 sound() 不直接写个空方法体,非要 abstract?

空方法体"忘了重写也不报错",错误会逃到运行时变成逻辑 bug;abstract 让编译器强制检查——子类没兑现就编译不过。把"忘写"从运行时事故提前到编译期拦截,这是抽象类的核心价值。

❓ Q2:抽象类能 new 吗?那它要构造方法干什么?

不能 new。但构造方法给子类用——子类对象创建时会先执行父类构造,给继承来的公共属性赋值。"不能实例化"和"没有构造方法"是两码事。

❓ Q3:`class Bird extends Animal implements Flyable, Singable` 合法吗?说明理由。

合法。继承是单选(Animal 只能一个爹,规矩不变),接口是多选(合同随便签,Flyable、Singable 都能要)。这正是 Java"单继承 + 多实现"绕开多继承冲突的完整解法。

❓ Q4:接口里写 `int MAX = 100;`,它自动带了哪三个修饰符?各管什么?

自动是 public static final——public 谁都能用(合同是公开的)、static 属于接口本身不用 new(3.7)、final 不许改(4.2)。接口里的"变量"其实全是常量。

❓ Q5:狗和飞机都会"移动",鸟和飞机都会"飞"。移动和飞该做成抽象类还是接口?为什么?

都是接口。"会移动""会飞"是跨家族的能力(can-do)——狗、飞机血缘完全不同,硬做成抽象类就得问"飞机是一种狗吗",is-a 念不通。能力做成接口(Movable、Flyable),谁有这本事谁 implements。


9. 下一步学什么


相关笔记