继承与多态
核心概念
继承(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
初始化顺序:
- 父类静态成员/静态块
- 子类静态成员/静态块
- 父类实例成员/实例块
- 父类构造函数
- 子类实例成员/实例块
- 子类构造函数
二、里氏替换原则(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
动态绑定过程:
- JVM获取对象的实际类型(Child)
- 在实际类型的方法表中查找方法
- 如果找到,调用该方法
- 如果没找到,向父类查找
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))
优先级规则:
- 精确匹配:参数类型完全一致
- 自动类型转换:基本类型自动提升
- 自动装箱/拆箱
- 可变参数
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 ➡️ 抽象类与接口
💬 评论