☕ 第四章 面向对象(下)

4.1 继承

📄 一句话概括

继承就是把几个类的公共成员收拢到一个父类,子类用一个 extends 全部白得——它是面向对象三大特性的第二张牌,也是 3.3~3.5 地基的第一次兑现。


1. 为什么需要继承:先看痛点

第三章你写过 Car 类。现在让你再写 Dog、Cat、Bird 三个类,会发现它们天天抄同一份作业:

class Dog {
    String name;
    int age;
    void eat()  { System.out.println(name + "在吃饭"); }
    void sleep(){ System.out.println(name + "在睡觉"); }
    void bark() { System.out.println(name + "汪汪叫"); }   // 独有
}

class Cat {
    String name;      // 又抄一遍
    int age;          // 又抄一遍
    void eat()  {...} // 又抄一遍
    void sleep(){...} // 又抄一遍
    void catchMouse() { ... }  // 独有
}

三个坑:

  1. 抄三遍:同样的代码出现三次,文件越来越厚;
  2. 改三处:需求变了(比如给 eat 加个日志),三个类挨个改,漏一处就出 bug——这正好是 3.3 说的"把变化圈在一处"的反面教材;
  3. 不成体系:代码读不出来"猫和狗都是动物"这个常识。

继承就是解药:把公共部分抽成一个 Animal(父类),Dog/Cat/Bird 各自 extends 它,抄写变成"白得"。


2. extends 语法:五分钟上手

// 父类:装公共成员
class Animal {
    String name;
    int age;
    void eat()  { System.out.println(name + "在吃饭"); }
    void sleep(){ System.out.println(name + "在睡觉"); }
}

// 子类:只写自己的绝活
class Dog extends Animal {
    void bark() { System.out.println(name + "汪汪叫"); }
    // name、age、eat()、sleep() 一个字没写,全都能用!
}
// 测试
Animal a = new Animal();
a.name = "动物";
a.eat();          // 动物在吃饭

Dog d = new Dog();
d.name = "旺财";   // 白得的属性,直接用
d.eat();          // 白得的方法,直接喊 → 旺财在吃饭
d.bark();         // 自己的绝活 → 旺财汪汪叫

术语对照(教材叫法不一,都认识):

叫法A 叫法B 谁是谁
父类 超类 / 基类 Animal,被继承的
子类 派生类 Dog,继承别人的

读法:Dog extends Animal = "狗是一种动物"——is-a 关系。能不能用继承,拿这句话检验:能自然念成"X 是一种 Y",才继承;"汽车是一种引擎"就念不通,那不是继承,是"包含"(下一章的组合)。


3. 继承了什么、没继承什么(本节考试中心)

❗ 子类得到的是父类的**非 private 成员**
  • public / 默认权限的属性和方法 → 白得,直接用
  • private 成员 → 被"继承"了但子类不能直接访问——锁还在(3.3 的 private 就是干这个的),子类想动只能走 getter/setter 这两扇门
  • 构造方法不被继承——构造方法是"造这个类的专用流水线",Dog 的出生流程和 Animal 不同,所以不传。但子类构造时会先找父类构造报到,这是 4.2/4.3 的内容,先混脸熟
class Animal {
    private int secret = 42;          // private 上锁
    public int getSecret() {          // 门卫
        return secret;
    }
}

class Dog extends Animal {
    void test() {
        // System.out.println(secret);      // 编译报错!锁着呢
        System.out.println(getSecret());    // 走门卫 → 42
    }
}

看到没?继承和封装不打架,反而配合默契:private 挡住子类的手,getter/setter 开受控的门。3.3 的努力在这里全部生效。


4. 三条规矩(记牢,考试常客)

规矩 1:Java 是单继承

class Dog extends Animal { }      // ✅ 一个爹
class Dog extends Animal, Robot { } // ❌ 编译报错:不能有两个直接爹

为什么?两个爹各有一个 eat(),你听谁的?——多继承的冲突问题。Java 用"单继承 + 接口"(后面章节)绕开这个坑。C++ 支持多继承,代价是著名的"钻石问题",Java 不背这个锅。

规矩 2:可以多层继承(爷孙链)

Animal(爷爷)
  ↓ extends
Dog(爸爸)
  ↓ extends
GuideDog(导盲犬,孙子)

GuideDog 白得 Animal 和 Dog 的全部非 private 成员——属性一代代传下去。但 Java 不支持循环继承(A 继承 B、B 又继承 A,编译器直接报错)。

规矩 3:Object 是万物之祖

你写的任何类,不写 extends 时默认继承 Object。所以每个对象都有 toString()、equals() 这些方法——你没写过它们却能调用,谜底在这。继承链的最顶端永远是 Object。


5. 继承在内存里长什么样

延续全章的"栈钥匙堆实物"暗线:

Dog d = new Dog();
                ↓
栈:d(一把钥匙)
                ↓ 指向
堆:一个 Dog 对象
    ├─ 独有部分:bark() 相关
    └─ 继承部分:name、age(从 Animal 图纸上"抄"进这个对象的格子里)

注意关键点:继承不是"链接到父类",而是 new 的时候把父类的成员直接搬进这个对象的格子里。改 d.name 只改这一只狗,Animal 类本身和别的对象毫发无伤。


6. 什么时候该用继承(动手前的判断课)

用 is-a 三问:

  1. 能不能自然念出"子类是一种父类"?(狗是一种动物 ✅ / 汽车是一种引擎 ❌)
  2. 子类是父类的特殊版本吗?(子类 = 父类 + 自己的绝活)
  3. 这些公共成员以后都会变吗?会变 → 抽到父类只改一处(3.3 的"圈住变化")

三个都答"是",才动手继承。继承不是"代码放哪方便"的问题,是"世界怎么分类"的问题——这也是它叫"面向对象思想"的原因。


7. 动手练习(保姆式,在 IDEA04 项目里)

📝 一次一步

① 新建项目 IDEA04(步骤同 1.5:File → New → Project → Java,JDK 选 17) ② src 上右键 → New → Java Class → 输入 Animal,把第 2 节的 Animal 类抄进去 ③ 同法建 Dog 类,只抄 extends Animal 那两行 ④ 同法建 TestAnimal,抄下面代码,点 main 旁边的绿三角 ▶

public class TestAnimal {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.name = "旺财";
        d.age = 2;
        d.eat();    // 旺财在吃饭  ← 一个字没写,凭空有了
        d.sleep();  // 旺财在睡觉
        d.bark();   // 旺财汪汪叫  ← 自己的绝活
    }
}

成功标准:三行输出一字不差。特别是第一行——Dog 类里明明没有 eat(),它照样能跑,这就是"白得"的现场证据。

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

  1. private 实验:给 Animal 的 age 前加 private,回 Dog 类看 d.age = 2 报什么错——亲眼看"继承不等于随便访问";
  2. 单继承实验:再建一个 Robot 类,让 Dog 写成 extends Animal, Robot,看编译器怎么拒绝;
  3. Object 实验:新建一个空类 class Empty { },在 Test 里敲 new Empty().toString();——空类凭什么有方法?规矩 3 的现场证明。

8. 常见误区

⚠️ 误区 1:private 成员"没有被继承"

更准确的说法:被继承了,但子类不能直接访问。成员确实进了子类对象(反射能摸到),只是 private 的锁还在。初学阶段记"private 子类用不了,要走 getter/setter"就够。

⚠️ 误区 2:构造方法也能继承

不能。构造方法是"造这个类的专用流程",Dog 有 Dog 的出生方式。但子类构造会先调用父类构造(super 的活),4.3 见。

⚠️ 误区 3:父类的东西子类全都要

抽父类前先问 is-a。为了"省代码"硬把不相干的类接成父子(比如让 Tree extends House 因为都"有高度"),代码是省了,世界观塌了。宁可不继承,不可错继承。

⚠️ 误区 4:Java 支持多继承,只是写法特殊

Java 是单继承,没有"特殊写法"。多继承的需求后面用接口(一个类可实现多个接口)满足,那是别的机制,不是继承。

⚠️ 误区 5:继承会拖慢程序

白得成员是编译期就确定的,运行时零开销。慢的从来不是继承,是写错的代码。


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

❓ Q1:`class Cat extends Animal` 之后,Cat 里能直接写 `name` 吗?前提是什么?

能,前提是 Animal 的 name 不是 private。private 成员被继承但上锁,子类不能直接碰,只能走 getter/setter。

❓ Q2:Java 为什么只允许单继承?

避免冲突:两个父类都有同名方法时,编译器无法裁决听谁的(C++ 的"钻石问题")。Java 的取舍:单继承保简单,多需求用接口补。

❓ Q3:`class A { }` 什么都没写,它有父类吗?

有——默认继承 Object。所以它天生拥有 toString()、equals() 等方法。所有类的继承链顶端都是 Object。

❓ Q4:下面的判断哪些成立?

① GuideDog extends Dog,Dog extends Animal → GuideDog 能用 Animal 的成员 ② Dog 能同时继承 Animal 和 Robot ③ 子类能直接访问父类的 private 属性 ①成立(多层继承,爷孙链上的都白得);②错(单继承);③错(private 上锁,走门卫)。

❓ Q5:想判断"该不该用继承",口诀是什么?拿"轮胎和汽车"练一遍。

is-a 口诀:能自然念成"子是一种父"才继承。"轮胎是一种汽车" ❌ ——念不通,轮胎和汽车是"整体-零件"关系,不该继承。真正成立的是"跑车是一种汽车"。


10. 下一步学什么


相关笔记