Java 继承与多态:概念、关键机制与容易踩的坑,一篇读懂

适合学过一遍快速抓重点。

写类的时候,总会遇到这样的问题:两个类看起来七八成相似,能不能把重复的部分抽出来?能不能让代码更灵活一些,而不是一堆 if-else 管理不同对象?继承与多态,正是为解决这些问题而设计的工具。


一、继承:把共性抽出来,让代码长得更干净

继承是面向对象里非常基础但极其重要的概念。它解决的核心问题只有一个:

把多个类中重复的代码抽取到一个更通用的父类中,实现复用。

于是:

  • 子类获得父类的属性和方法;
  • 子类只关注“新增”部分;
  • 整个体系形成一种“is-a(是一个)”关系。

例如“狗是一种动物”、 “猫是一种动物”,就是典型继承关系。

1. 继承的写法

class 子类 extends 父类 {
    // 自己新增的内容
}

子类会自动继承父类的成员变量和方法。

2. 成员访问的规则:就近原则

不论是访问变量还是方法,都遵循同一个原则:

自己有用自己的,没找到再去父类里找。

相比变量,方法还有一个额外情况:如果同名但参数列表不同,那属于重载,由参数类型决定调用哪个。

3. 与父类同名的成员:super 上场

如果子类和父类都有同名的字段或方法:

  • 默认访问子类自己的;
  • 想访问父类的,用 super.成员名

例如:

super.a = 200;
super.methodB();

4. 子类构造方法:永远先构造父,再构造子

Java 规定:

创建子类对象时,必须先执行父类构造方法,再执行子类构造方法。

如果你不写 super(...),编译器也会自动加上 super()

但要注意:

  • 如果父类没有无参构造,你必须手动写 super(参数)
  • super(...) 必须是构造方法第一行;
  • super(...)this(...) 不能同时出现,只能二选一。

5. 初始化顺序(继承情况下)

当父类和子类都存在静态代码块、实例代码块、构造器时,整体执行顺序:

  1. 父类静态代码块
  2. 子类静态代码块
  3. 父类实例代码块
  4. 父类构造方法
  5. 子类实例代码块
  6. 子类构造方法

静态内容只执行一次,但实例内容会在每次 new 时执行。

6. protected:让子类“看得见”

访问权限在继承关系里有所变化:

  • private:子类看不见(但确实继承了)。
  • default:子类必须在同包才能访问。
  • protected:不同包的子类也能访问。
  • public:谁都能访问。

这是设计 API 时非常核心的决策点,关系到封装与扩展。

7. final:继承体系的“终点标志”

  • 修饰类 → 不能被继承(如 String
  • 修饰方法 → 不能被子类重写
  • 修饰变量 → 常量

继承层次过深会让系统难维护,所以某些类被设计成 final,就是为了防止复杂继承。

8. 继承 vs 组合:别被继承迷住眼

继承是“is-a”,组合是“has-a”:

  • 汽车 轮胎 → 组合
  • 动物 → 继承

如果只是复用行为,大多数场景组合比继承更灵活,不容易造成层次过深。

一般建议:能用组合尽量用组合


二、多态:让同一段代码具备“根据对象改变行为”的能力

多态这个词,看起来玄,其实一句话就能解释:

父类引用指向子类对象,根据实际对象类型决定行为。

1. 多态成立的条件(三件套)

必须同时满足:

  1. 有继承关系
  2. 子类重写了父类的方法
  3. 通过父类引用调用该方法

例如:

Animal a = new Dog();
a.eat();  // 调用的是 Dog 重写的 eat()

2. 重写:外壳不变,核心重写

重写(override)的核心要求:

  • 方法名相同
  • 参数列表相同
  • 返回值类型相同(或是协变类型)
  • 访问权限不能更严格(如父类 public,子类不能写 protected)
  • 不能重写 private、static、final、构造方法

建议始终写上 @Override 注解,能帮你避免拼写错误等低级 bug。

3. 方法绑定:重载靠编译期,重写靠运行期

  • 重载(overload) = 静态绑定(编译期决定)
  • 重写(override) = 动态绑定(运行期决定)

多态就是靠“动态绑定”实现的。

4. 向上转型:让代码更通用

把子类对象当成父类引用:

Animal a = new Cat();

好处:

  • 能写出更通用、扩展性更强的代码
  • 方法参数、返回值都可以统一成“父类型”

缺点:

  • 父类引用看不到子类独有方法(比如不能调用 cat.mew()

5. 向下转型:小心使用!

如果需要访问子类独有方法,需要把父类引用转回子类类型:

Animal a = new Dog();
Dog d = (Dog)a;   // OK

但如果类型不匹配,就会运行时抛异常:

Animal a = new Dog();
Cat c = (Cat)a;   // 运行时 ClassCastException

因此通常与 instanceof 配合使用:

if (a instanceof Cat) {
    Cat c = (Cat)a;
    c.mew();
}

这个道理就是,把“狗”赋值给“动物”,是向上转型,因为狗本来就是诸多动物这个大集合中的一种。而把“动物”赋值给“狗”就是向下转型了,因为“动物”不一定是狗,就要加上强制转换的括号。


三、多态的好处与缺点

好处 1:减少 if-else,降低复杂度

没有多态时:

if (shape.equals("rect")) { ... }
else if (shape.equals("cycle")) { ... }

有了多态:

Shape[] shapes = {new Rect(), new Cycle(), ...};
for (Shape s : shapes) {
    s.draw();  // 自动选择对应实现
}

代码更干净,也更容易扩展。

好处 2:扩展自然,不会破坏原代码

新增一个 Flower 类,只要保证继承自 Shape 并重写 draw(),调用代码完全不需要改动。


多态的缺陷

  1. 属性没有多态性
    父类引用访问的永远是父类字段,不是子类的。
  2. 构造方法没有多态
    动态绑定不作用于构造器。
  3. 构造器中使用可被重写的方法,会引发难查错误

例如:

class B {
    public B() { func(); }
    public void func() {}
}

class D extends B {
    private int num = 1;
    @Override
    public void func() {
        System.out.println(num); // num 还没初始化
    }
}

输出可能是 0,而不是 1,因为子类尚未初始化。

因此建议 不要在构造方法里调用可被重写的方法


四、继承与多态的关键易错点(高频考点)

你可以把这一节当作“复习清单”:

继承部分

  • 子类构造方法一定会先执行父类构造方法
  • 父类没有无参构造时,子类必须显式写 super(参数)
  • super(...) 必须是构造方法第一行
  • super(...)this(...) 不能同时出现
  • 同名变量访问遵循就近原则
  • 访问被隐藏的父类变量/方法需用 super
  • private 的成员虽然继承下来,但不可访问
  • default 成员不同包子类不可见
  • protected 成员不同包子类可见
  • 一个类能否继承另一个类也可能受 final 限制

多态部分

  • 多态必须基于继承 & 重写 & 父类引用
  • static / private / final 方法无法重写,因此不具备多态性
  • 字段没有多态性
  • 向下转型一定要用 instanceof 判断
  • 重写的方法要求“方法签名一致+访问权限不更严格”
  • 构造方法不要调用可被重写的方法

五、总结:继承与多态,别只是“会写”,要知道“为什么这样写”

继承让你减少重复代码,多态让你写出可扩展、可维护的结构。
但要记住,继承不是越多越好,多态也不是到处乱用。
真正的目标,是写出清晰、可扩展、安全的代码结构。

继承让你塑造类型体系,多态让你构建可扩展系统,而接口让整个系统摆脱继承链的束缚。


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐