继承与多态

核心概念

继承(Inheritance)和多态(Polymorphism)是面向对象编程的核心特性。继承实现代码复用,多态实现灵活扩展。理解它们的底层机制,是写出优雅OOP代码的基础。


一、继承(Inheritance)

1.1 基本语法

// 父类(基类/超类)
public class Animal {
    protected String name;

    public void eat() {
        System.out.println(name + " is eating");
    }
}

// 子类(派生类)
public class Dog extends Animal {
    public void bark() {
        System.out.println(name + " is barking");
    }
}

// 使用
Dog dog = new Dog();
dog.name = "Buddy";
dog.eat();   // 继承自Animal
dog.bark();  // Dog特有

1.2 Java继承的特点

特点 说明 示例
单继承 一个类只能有一个直接父类 class A extends B
多层继承 可以有继承链 A→B→C→Object
传递性 子类拥有所有祖先的非私有成员 C可访问A、B的成员
根类Object 所有类都隐式继承Object 默认有toString()等方法

1.3 继承中的构造函数

class Parent {
    protected String name;

    public Parent() {
        System.out.println("Parent无参构造");
    }

    public Parent(String name) {
        this.name = name;
        System.out.println("Parent有参构造");
    }
}

class Child extends Parent {
    private int age;

    public Child() {
        // 隐式调用 super()
        System.out.println("Child无参构造");
    }

    public Child(String name, int age) {
        super(name);  // 显式调用父类有参构造,必须在第一行
        this.age = age;
        System.out.println("Child有参构造");
    }
}

// 测试
new Child();
// 输出:Parent无参构造 → Child无参构造

new Child("Tom", 20);
// 输出:Parent有参构造 → Child有参构造

构造函数规则:

  • 子类构造函数必须调用父类构造函数
  • 如果不显式调用,编译器自动插入super()
  • 如果父类没有无参构造,子类必须显式调用有参构造
  • super()必须在构造函数第一行

1.4 super关键字

public class Child extends Parent {
    private String name;  // 与父类同名字段

    public Child(String name) {
        super(name);  // 1. 调用父类构造函数
        this.name = name;
    }

    @Override
    public void eat() {
        super.eat();  // 2. 调用父类方法
        System.out.println("Child eating");
    }

    public void showName() {
        System.out.println(this.name);   // 子类的name
        System.out.println(super.name);  // 3. 访问父类字段
    }
}

1.5 初始化顺序

class Parent {
    static { System.out.println("1. Parent静态块"); }
    { System.out.println("3. Parent实例块"); }
    Parent() { System.out.println("4. Parent构造函数"); }
}

class Child extends Parent {
    static { System.out.println("2. Child静态块"); }
    { System.out.println("5. Child实例块"); }
    Child() { System.out.println("6. Child构造函数"); }
}

new Child();
// 输出顺序:1 → 2 → 3 → 4 → 5 → 6

初始化顺序:

  1. 父类静态成员/静态块
  2. 子类静态成员/静态块
  3. 父类实例成员/实例块
  4. 父类构造函数
  5. 子类实例成员/实例块
  6. 子类构造函数

二、里氏替换原则(LSP)

2.1 定义

所有引用基类的地方必须能透明地使用其子类的对象。 —— Barbara Liskov

简单说:子类对象必须能够替换父类对象,且程序行为不变。

2.2 正确示例

class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

// 正确:正方形不应该继承矩形
class Square {
    private int side;
    public void setSide(int side) { this.side = side; }
    public int getArea() { return side * side; }
}

2.3 违反LSP的示例

// ❌ 错误设计:正方形继承矩形
class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;  // 强制相等
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;  // 强制相等
    }
}

// 问题:使用父类引用时行为不一致
void resize(Rectangle r) {
    r.setWidth(5);
    r.setHeight(10);
    assert r.getArea() == 50;  // Rectangle: 50, Square: 100 ❌
}

2.4 LSP的约束

约束 说明
前置条件不能加强 子类方法的输入参数要求不能比父类更严格
后置条件不能削弱 子类方法的输出结果不能比父类更宽松
不变式必须保持 父类的约束条件子类必须遵守
历史约束 子类不能修改父类的状态变化规则

三、多态(Polymorphism)

3.1 多态的三个必要条件

// 1. 继承关系
class Animal {
    public void speak() { System.out.println("Animal"); }
}
class Dog extends Animal {
    @Override
    public void speak() { System.out.println("Woof"); }
}
class Cat extends Animal {
    @Override
    public void speak() { System.out.println("Meow"); }
}

// 2. 方法重写(见上)

// 3. 向上转型(父类引用指向子类对象)
Animal a1 = new Dog();
Animal a2 = new Cat();

a1.speak();  // Woof(运行时调用Dog的方法)
a2.speak();  // Meow(运行时调用Cat的方法)

3.2 编译时类型 vs 运行时类型

Animal animal = new Dog();
//  ↑ 编译时类型    ↑ 运行时类型

// 编译时:检查Animal类是否有该方法
// 运行时:调用实际对象(Dog)的方法

animal.speak();  // ✅ Animal有speak(),运行时调用Dog.speak()
animal.bark();   // ❌ 编译错误:Animal没有bark()方法

3.3 向上转型与向下转型

// 向上转型(自动,安全)
Animal animal = new Dog();

// 向下转型(强制,需要检查)
Dog dog = (Dog) animal;  // ✅ 实际是Dog对象
Cat cat = (Cat) animal;  // ❌ ClassCastException

// 安全的向下转型
if (animal instanceof Dog) {
    Dog d = (Dog) animal;
    d.bark();
}

// JDK 16+ 模式匹配
if (animal instanceof Dog d) {
    d.bark();  // 直接使用d
}

3.4 动态绑定(方法调用机制)

class Parent {
    public void method() { System.out.println("Parent"); }
}

class Child extends Parent {
    @Override
    public void method() { System.out.println("Child"); }
}

Parent p = new Child();
p.method();  // 输出:Child

动态绑定过程:

  1. JVM获取对象的实际类型(Child)
  2. 在实际类型的方法表中查找方法
  3. 如果找到,调用该方法
  4. 如果没找到,向父类查找

3.5 方法调用优先级

当存在继承、重写、重载时,方法调用遵循以下优先级:

class Parent {
    public void method(Object obj) {
        System.out.println("Parent-Object");
    }
}

class Child extends Parent {
    public void method(Object obj) {
        System.out.println("Child-Object");
    }

    public void method(String str) {
        System.out.println("Child-String");
    }
}

Child c = new Child();
c.method("hello");  // Child-String(优先匹配更精确的参数类型)
c.method(new Object());  // Child-Object

Parent p = new Child();
p.method("hello");  // Child-Object(编译时确定调用method(Object))

优先级规则:

  1. 精确匹配:参数类型完全一致
  2. 自动类型转换:基本类型自动提升
  3. 自动装箱/拆箱
  4. 可变参数

3.6 静态绑定 vs 动态绑定

特性 静态绑定 动态绑定
绑定时机 编译时 运行时
适用方法 static、private、final、构造方法 普通实例方法
依据 引用类型(编译时类型) 对象类型(运行时类型)
class Parent {
    public static void staticMethod() { System.out.println("Parent static"); }
    public void instanceMethod() { System.out.println("Parent instance"); }
}

class Child extends Parent {
    public static void staticMethod() { System.out.println("Child static"); }
    @Override
    public void instanceMethod() { System.out.println("Child instance"); }
}

Parent p = new Child();
p.staticMethod();    // Parent static(静态绑定,看引用类型)
p.instanceMethod();  // Child instance(动态绑定,看对象类型)

四、字段的继承与隐藏

4.1 字段不存在多态

class Parent {
    public String name = "Parent";
}

class Child extends Parent {
    public String name = "Child";  // 隐藏父类字段,不是重写
}

Parent p = new Child();
System.out.println(p.name);  // Parent(字段访问看引用类型)

Child c = new Child();
System.out.println(c.name);  // Child
System.out.println(((Parent)c).name);  // Parent

4.2 字段隐藏 vs 方法重写

特性 字段隐藏 方法重写
多态 不支持 支持
访问依据 引用类型 对象类型
父类访问 super.field super.method()

五、高频面试题

Q1: Java为什么不支持多继承?

答:

  • 菱形继承问题:如果A继承B和C,B和C都有同名方法,A调用哪个?
  • 复杂性:多继承增加语言复杂度和编译器实现难度
  • 替代方案:Java通过接口实现多继承的效果(JDK 8+接口可以有默认方法)

Q2: 重写和重载的区别?

答:

特性 重写(Override) 重载(Overload)
位置 子类与父类之间 同一个类中
方法名 相同 相同
参数列表 相同 必须不同
返回类型 相同或协变 无要求
多态类型 运行时多态 编译时多态

Q3: 什么是里氏替换原则?

答:

  • 子类对象必须能够替换父类对象
  • 程序行为不应该因为替换而改变
  • 违反LSP的设计会导致代码脆弱,难以维护

Q4: 静态方法能被重写吗?

答:

  • 不能。静态方法属于类,不属于对象
  • 子类可以定义同名静态方法,但这是隐藏,不是重写
  • 调用时根据引用类型决定,不存在多态

Q5: 构造函数能被重写吗?

答:

  • 不能。构造函数不是普通方法,不能被继承
  • 子类构造函数必须调用父类构造函数(显式或隐式)
  • 构造函数名必须与类名相同,子类不可能有父类同名的构造函数

Q6: 向上转型和向下转型的区别?

答:

// 向上转型:自动,安全
Animal a = new Dog();

// 向下转型:强制,可能失败
Dog d = (Dog) a;  // 需要instanceof检查

Q7: 为什么字段不支持多态?

答:

  • 字段访问在编译时确定,根据引用类型
  • 方法调用在运行时确定,根据对象类型
  • 这是Java的设计选择,保证字段访问的效率

六、最佳实践

✅ 推荐做法

// 1. 使用@Override注解
@Override
public void method() { }

// 2. 遵循里氏替换原则
// 子类不应该改变父类的行为契约

// 3. 优先使用组合而非继承
class Car {
    private Engine engine;  // 组合
    // 而不是 class Car extends Engine
}

// 4. 向下转型前检查类型
if (animal instanceof Dog dog) {
    dog.bark();
}

// 5. 使用多态简化代码
List<Animal> animals = Arrays.asList(new Dog(), new Cat());
for (Animal a : animals) {
    a.speak();  // 多态调用
}

❌ 避免做法

// 1. 滥用继承
class Stack extends ArrayList { }  // 错误:Stack不是ArrayList

// 2. 违反LSP
class Square extends Rectangle { }  // 正方形不应该继承矩形

// 3. 不检查就向下转型
Dog d = (Dog) animal;  // 危险

// 4. 在构造函数中调用可重写方法
class Parent {
    Parent() {
        method();  // 危险:子类还未初始化
    }
    public void method() { }
}

相关链接


记忆口诀

继承单一多层传,构造必须调父先
多态三要:继承、重写、向上转
编译看左运行右,动态绑定是关键
里氏替换要遵守,子类替父行为同
字段隐藏无多态,静态方法看引用

⬅️ BigDecimal 🏠 00-Java ➡️ 抽象类与接口