继承和多态
# 08.继承和多态
# 目录介绍
# 8.1 继承基础
# 8.1.1 继承基本概念
继承是面向对象编程的核心特性之一,允许一个类(子类/派生类)基于另一个类(父类/基类)创建,从而复用父类的属性和方法。
父类(基类): Animal
↑
子类(派生类):Dog, Cat
2
3
# 8.1.2 继承的语法
使用 extends 关键字实现继承:
// 父类
public class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + " 在吃东西");
}
}
// 子类
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // 调用父类构造方法
this.breed = breed;
}
public void bark() {
System.out.println(name + " 在汪汪叫");
}
}
Dog dog = new Dog("旺财", 3, "金毛");
dog.eat(); // 继承自 Animal
dog.bark(); // Dog 自己的方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
对比 C++:C++ 使用 : 加访问修饰符继承(class Dog : public Animal),Java 使用 extends。C++ 没有访问修饰符限制,默认 private 继承;Java 只有 public 继承。
# 8.1.3 Java单继承
Java 只支持单继承,一个类只能有一个直接父类:
// 正确:单继承
class Dog extends Animal {}
// 错误:Java 不支持多继承
// class Dog extends Animal, Pet {} // 编译错误
// 但可以通过接口实现"多继承"的效果
class Dog extends Animal implements Pet, Trainable {}
2
3
4
5
6
7
8
对比 C++:C++ 支持多继承(class Dog : public Animal, public Pet {}),但多继承容易导致菱形继承问题。Java 通过接口避免了这个问题。
# 8.1.4 继承中的构造方法
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal 构造方法");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name); // 必须在第一行调用父类构造方法
System.out.println("Dog 构造方法");
}
}
Dog dog = new Dog("旺财");
// 输出:
// Animal 构造方法
// Dog 构造方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
构造顺序:先父类后子类。如果父类没有无参构造方法,子类必须显式调用
super(参数)。
# 8.1.5 综合案例:员工继承体系
编写一个员工类继承体系,展示继承的基本语法、构造方法链和代码复用。
public class EmployeeDemo {
static class Employee {
protected String name;
protected double baseSalary;
Employee(String name, double baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
System.out.println("[Employee构造] " + name);
}
public double getSalary() { return baseSalary; }
public String toString() { return name + ", 薪资: ¥" + getSalary(); }
}
static class Manager extends Employee {
private double bonus;
Manager(String name, double baseSalary, double bonus) {
super(name, baseSalary); // 调用父类构造
this.bonus = bonus;
System.out.println("[Manager构造] 奖金: " + bonus);
}
public double getSalary() { return baseSalary + bonus; }
}
static class Developer extends Employee {
private int overtimeHours;
Developer(String name, double baseSalary, int overtimeHours) {
super(name, baseSalary);
this.overtimeHours = overtimeHours;
}
public double getSalary() { return baseSalary + overtimeHours * 100; }
}
public static void main(String[] args) {
System.out.println("===== 创建对象(观察构造链) =====");
Employee emp = new Employee("张三", 8000);
Manager mgr = new Manager("李四", 12000, 5000);
Developer dev = new Developer("王五", 10000, 20);
System.out.println("\n===== 员工信息 =====");
Employee[] team = {emp, mgr, dev};
for (Employee e : team) {
System.out.println(" " + e);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 8.1.6 继承基础训练题
训练1:创建一个三层继承链:Vehicle → Car → ElectricCar。Vehicle 有属性 brand 和 maxSpeed,Car 新增 fuelType,ElectricCar 新增 batteryCapacity。编写构造方法并测试构造顺序。
训练2:以下代码输出什么?请画出构造方法的调用链:
class A { A() { System.out.println("A"); } }
class B extends A { B() { System.out.println("B"); } }
class C extends B { C() { System.out.println("C"); } }
new C();
2
3
4
思考:Java 中子类构造方法的第一行必须是 super() 或 this()。如果两者都不写,编译器会自动插入 super()。这种强制机制保证了什么?(提示:父类字段的初始化安全)
# 8.2 方法重写
# 8.2.1 方法重写概念
子类可以重写(Override)父类的方法,提供自己的实现:
public class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵喵!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 8.2.2 Override注解
@Override 注解不是必须的,但强烈推荐使用。它让编译器帮你检查是否正确重写了父类方法:
@Override
public void makeSound() { // 如果方法签名写错,编译器会报错
System.out.println("汪汪汪!");
}
2
3
4
# 8.2.3 重写和重载区别
| 对比项 | 重写(Override) | 重载(Overload) |
|---|---|---|
| 发生位置 | 子类和父类之间 | 同一个类中 |
| 方法名 | 相同 | 相同 |
| 参数列表 | 相同 | 必须不同 |
| 返回类型 | 相同或协变类型 | 可以不同 |
| 访问修饰符 | 不能更严格 | 无限制 |
# 8.2.4 综合案例:动物叫声重写
编写一个程序,展示方法重写的正确写法、@Override 注解的作用,以及重写与重载的对比。
public class OverrideDemo {
static class Animal {
public String speak() { return "..."; }
public String toString() { return "Animal"; }
}
static class Dog extends Animal {
@Override
public String speak() { return "汪汪汪!"; }
@Override
public String toString() { return "狗"; }
}
static class Cat extends Animal {
@Override
public String speak() { return "喵喵喵~"; }
@Override
public String toString() { return "猫"; }
// 重载(不是重写): 参数不同
public String speak(int times) {
return "喵~".repeat(times);
}
}
public static void main(String[] args) {
Animal[] animals = {new Dog(), new Cat(), new Animal()};
System.out.println("===== 方法重写 =====");
for (Animal a : animals) {
System.out.println(a + " 说: " + a.speak()); // 运行时调用实际类型的方法
}
System.out.println("\n===== 重写vs重载 =====");
Cat cat = new Cat();
System.out.println("重写(无参): " + cat.speak());
System.out.println("重载(3次): " + cat.speak(3));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 8.2.5 方法重写训练题
训练1:以下代码能编译通过吗?如果不能,如何修改?
class Parent {
Object getValue() { return "parent"; }
}
class Child extends Parent {
String getValue() { return "child"; } // 协变返回类型?
}
2
3
4
5
6
训练2:子类重写方法时,访问权限只能放大不能缩小。请编写代码验证:父类方法是 protected,子类重写为 private 会发生什么?
思考:static 方法能被重写吗?如果父类和子类有同名同参数的 static 方法,这叫什么?(提示:隐藏 hiding vs 重写 overriding)
# 8.3 super关键字
# 8.3.1 super访问父类
public class Dog extends Animal {
@Override
public void makeSound() {
super.makeSound(); // 调用父类的方法
System.out.println("汪汪汪!");
}
}
2
3
4
5
6
7
# 8.3.2 super调用构造方法
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // 调用父类构造方法,必须在第一行
this.breed = breed;
}
}
2
3
4
5
6
7
8
对比 C++:C++ 使用初始化列表调用父类构造 Dog(string name) : Animal(name) {},Java 使用 super() 在构造方法体中调用。
# 8.3.3 综合案例:super调用链演示
编写一个三层继承体系,展示 super 访问父类属性、方法和构造方法的完整用法。
public class SuperChainDemo {
static class Vehicle {
protected String brand;
Vehicle(String brand) {
this.brand = brand;
System.out.println("Vehicle(" + brand + ")");
}
public String info() { return "品牌: " + brand; }
}
static class Car extends Vehicle {
protected int seats;
Car(String brand, int seats) {
super(brand); // 调用父类构造
this.seats = seats;
System.out.println("Car(seats=" + seats + ")");
}
@Override
public String info() {
return super.info() + ", 座位: " + seats; // super调用父类方法
}
}
static class ElectricCar extends Car {
private int battery;
ElectricCar(String brand, int seats, int battery) {
super(brand, seats); // 调用Car的构造
this.battery = battery;
System.out.println("ElectricCar(battery=" + battery + ")");
}
@Override
public String info() {
return super.info() + ", 电池: " + battery + "kWh";
}
}
public static void main(String[] args) {
System.out.println("===== 构造方法链(自顶向下) =====");
ElectricCar tesla = new ElectricCar("Tesla", 5, 75);
System.out.println("\n===== super方法链 =====");
System.out.println(tesla.info());
// 输出: 品牌: Tesla, 座位: 5, 电池: 75kWh
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 8.3.4 super关键字训练题
训练1:编写一个 LoggingList 继承 ArrayList,重写 add() 方法,在调用 super.add() 之前打印日志信息。
训练2:this 和 super 的区别是什么?以下代码中 this() 和 super() 能同时出现在一个构造方法中吗?为什么?
class Child extends Parent {
Child() {
// super(); // 调用父类构造
// this(10); // 调用本类其他构造
// 能同时存在吗?
}
Child(int x) { super(); }
}
2
3
4
5
6
7
8
思考:super 关键字不是一个对象引用,不能赋给变量(Object o = super; 编译错误)。它在JVM层面是如何实现的?(提示:编译器在编译时就确定了 super 指向的父类方法)
# 8.4 Object类
# 8.4.1 Object是所有类的父类
Java 中所有类都直接或间接继承自 Object 类。如果一个类没有显式 extends,则默认继承 Object。
public class Account { // 等价于 public class Account extends Object
}
2
# 8.4.2 toString方法
public class Account {
private String name;
private double balance;
@Override
public String toString() {
return "Account{name='" + name + "', balance=" + balance + "}";
}
}
Account acc = new Account("张三", 1000);
System.out.println(acc); // 自动调用 toString()
// 输出:Account{name='张三', balance=1000.0}
2
3
4
5
6
7
8
9
10
11
12
13
# 8.4.3 equals方法
public class Account {
private String name;
private double balance;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Account other = (Account) obj;
return name.equals(other.name) && Double.compare(balance, other.balance) == 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
# 8.4.4 hashCode方法
重写 equals 必须同时重写 hashCode:
@Override
public int hashCode() {
return Objects.hash(name, balance);
}
2
3
4
# 8.4.5 综合案例:正确重写equals和hashCode
编写一个 Point 类,正确重写 toString、equals 和 hashCode 方法,并验证其行为。
import java.util.HashSet;
import java.util.Objects;
public class ObjectMethodDemo {
static class Point {
private int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Point other)) return false;
return this.x == other.x && this.y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
public static void main(String[] args) {
Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
Point p3 = new Point(5, 6);
// toString
System.out.println("===== toString =====");
System.out.println(p1); // 自动调用toString
// equals
System.out.println("\n===== equals =====");
System.out.println("p1.equals(p2): " + p1.equals(p2)); // true
System.out.println("p1.equals(p3): " + p1.equals(p3)); // false
System.out.println("p1 == p2: " + (p1 == p2)); // false (不同对象)
// hashCode: equals相等→hashCode必须相等
System.out.println("\n===== hashCode =====");
System.out.println("p1.hashCode: " + p1.hashCode());
System.out.println("p2.hashCode: " + p2.hashCode());
System.out.println("相等: " + (p1.hashCode() == p2.hashCode()));
// HashSet验证
System.out.println("\n===== HashSet去重 =====");
HashSet<Point> set = new HashSet<>();
set.add(p1);
set.add(p2); // 因equals相等,不会重复添加
set.add(p3);
System.out.println("集合大小: " + set.size()); // 2
System.out.println("包含(3,4): " + set.contains(new Point(3, 4))); // true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 8.4.6 训练题
实践题:创建一个
Student类(姓名、学号),重写toString()、equals()、hashCode()三个方法。然后创建两个属性相同的Student对象,验证它们放入HashSet后是否被视为同一个元素。代码题:以下代码的输出是什么?请解释原因:
Object obj = new Object(); System.out.println(obj.toString()); System.out.println(obj.hashCode() == System.identityHashCode(obj));1
2
3思考题:
Object类的clone()方法是protected的,且要求实现Cloneable接口。为什么 Java 这样设计而不是把clone()设为public?浅拷贝和深拷贝的区别是什么?
# 8.5 多态
# 8.5.1 多态基本概念
多态:同一个方法在不同对象上有不同的表现形式。多态的前提:
- 有继承关系
- 子类重写父类方法
- 父类引用指向子类对象
Animal animal1 = new Dog("旺财", 3, "金毛"); // 父类引用指向子类对象
Animal animal2 = new Cat("咪咪", 2);
animal1.makeSound(); // 汪汪汪!(调用的是 Dog 的方法)
animal2.makeSound(); // 喵喵喵!(调用的是 Cat 的方法)
2
3
4
5
# 8.5.2 向上转型和向下转型
// 向上转型(自动):子类 → 父类
Animal animal = new Dog("旺财", 3, "金毛");
animal.makeSound(); // 可以调用 Animal 中声明的方法
// animal.bark(); // 编译错误!Animal 没有 bark 方法
// 向下转型(强制):父类 → 子类
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.bark(); // 现在可以调用 Dog 特有的方法了
}
// JDK 16+ 模式匹配
if (animal instanceof Dog dog) {
dog.bark(); // 直接使用
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 8.5.3 多态案例
public class Main {
// 多态的典型应用:方法参数使用父类类型
public static void animalShow(Animal animal) {
animal.makeSound(); // 运行时根据实际类型调用对应方法
}
public static void main(String[] args) {
animalShow(new Dog("旺财", 3, "金毛")); // 汪汪汪!
animalShow(new Cat("咪咪", 2)); // 喵喵喵!
}
}
2
3
4
5
6
7
8
9
10
11
# 8.5.4 多态的好处
- 提高代码可扩展性:新增子类不需要修改已有代码。
- 降低耦合度:调用方只依赖父类/接口,不依赖具体实现。
- 统一接口:通过父类类型统一管理不同子类对象。
对比 C++:C++ 通过虚函数(virtual)实现多态,Java 中所有非 static、非 final、非 private 方法默认都是"虚方法",不需要 virtual 关键字。
多态的底层原理(方法分派):JVM 通过虚方法表(vtable) 实现多态。每个类在加载时都会建立一张虚方法表,表中存储了该类所有可被动态调用的方法的入口地址。子类的虚方法表会继承父类的表项,重写的方法会替换为子类的实现地址。调用时,JVM 使用 invokevirtual 指令根据对象实际类型的虚方法表找到方法入口——这就是动态绑定(晚期绑定)。而字段访问、static 方法、private 方法和构造方法则是静态绑定(编译期确定),这也是为什么 p.name 访问的是父类字段而 p.show() 调用的是子类方法。
# 8.5.5 综合案例:图形面积计算器(多态)
编写一个程序,用多态实现不同图形的面积计算,展示向上转型、向下转型和多态的优势。
public class PolymorphismDemo {
static abstract class Shape {
abstract double area();
abstract String shapeName();
}
static class Circle extends Shape {
double radius;
Circle(double r) { this.radius = r; }
double area() { return Math.PI * radius * radius; }
String shapeName() { return "圆(r=" + radius + ")"; }
}
static class Rectangle extends Shape {
double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
double area() { return w * h; }
String shapeName() { return "矩形(" + w + "×" + h + ")"; }
}
static class Triangle extends Shape {
double base, height;
Triangle(double b, double h) { this.base = b; this.height = h; }
double area() { return 0.5 * base * height; }
String shapeName() { return "三角形(底=" + base + ",高=" + height + ")"; }
}
// 多态:接收父类类型,处理所有子类
static void printArea(Shape shape) {
System.out.printf(" %s → 面积 = %.2f%n", shape.shapeName(), shape.area());
}
public static void main(String[] args) {
// 向上转型:子类对象赋给父类引用
Shape[] shapes = {
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8),
new Circle(10)
};
System.out.println("===== 多态计算面积 =====");
double totalArea = 0;
for (Shape s : shapes) {
printArea(s); // 同一个方法,不同表现
totalArea += s.area();
}
System.out.printf("总面积: %.2f%n", totalArea);
// 向下转型(需要instanceof检查)
System.out.println("\n===== 向下转型 =====");
for (Shape s : shapes) {
if (s instanceof Circle c) { // JDK16+ 模式匹配
System.out.println(" 圆的半径: " + c.radius);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 8.5.6 多态训练题
训练1:以下代码中 animal.eat() 调用的是哪个版本?请从虚方法表角度解释:
class Animal { void eat() { System.out.println("Animal eat"); } }
class Dog extends Animal { void eat() { System.out.println("Dog eat"); } }
class GoldenRetriever extends Dog { void eat() { System.out.println("Golden eat"); } }
Animal animal = new GoldenRetriever();
animal.eat(); // 输出什么?
2
3
4
5
6
训练2:编写一个 Shape[] 数组,包含圆形、矩形、三角形等多种图形,使用多态调用 area() 方法计算总面积。然后使用 instanceof 分别统计每种图形的个数。
疑惑:多态中,成员变量是否也有动态绑定?
答疑:没有! Java 中成员变量是静态绑定的,在编译期就根据引用的声明类型决定访问哪个字段。只有方法才有动态绑定。
论证:
class Parent {
int value = 10;
void show() { System.out.println("Parent show"); }
}
class Child extends Parent {
int value = 20;
void show() { System.out.println("Child show"); }
}
Parent p = new Child();
System.out.println(p.value); // 10(静态绑定,看声明类型 Parent)
p.show(); // Child show(动态绑定,看实际类型 Child)
2
3
4
5
6
7
8
9
10
11
12
结果展示:p.value 输出 10 而不是 20,证明字段访问不走虚方法表,而是编译期直接确定。这个行为是 Java 面试的经典考题。