本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。
点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!
本文篇幅较长,建议先收藏再食用!
系列文章目录
JAVA学习 DAY2 java程序运行、注意事项、转义字符
JAVA学习 DAY5 变量&数据类型 [万字长文!一篇搞定!]
JAVA学习 DAY7 程序逻辑控制【万字长文!一篇搞定!】
JAVA学习 DAY11 类和对象_续1【万字长文!一篇搞定!】
JAVA学习 DAY12 继承和多态【万字长文!一篇搞定!】
JAVA学习 DAY13 抽象类和接口【万字长文!一篇搞定!】
深度剖析 Java 图书管理系统设计与实现:类、接口与对象的实战应用
拓展文章
Java避坑指南:千万别在构造方法中调用重写的方法!(附代码案例+执行流程全解析)
深入剖析 Java 中的深拷贝与浅拷贝:原理、实现与最佳实践
目录
前言
小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!

一、章节核心目标
- 掌握继承的概念、语法、核心机制及使用规范
- 理解组合的本质,能够在继承与组合之间做出合理选择
- 精通多态的实现条件、底层原理及实际应用场景
- 辨析重写与重载、向上转型与向下转型等易混淆概念
- 规避继承与多态使用过程中的常见陷阱
二、继承(Inheritance)
2.1 为什么需要继承
Java 中通过类描述现实世界的实体,实例化后的对象对应具体实体。但现实事物存在复杂关联,如狗和猫都属于动物,它们具有姓名、年龄、体重等共性特征,以及吃饭、睡觉等共性行为,同时也有各自的特有行为(狗吠、猫叫)。
若单独设计Dog类和Cat类,会出现大量重复代码:
// Dog.java
public class Dog{
String name;
int age;
float weight;
public void eat(){
System.out.println(name + "正在吃饭");
}
public void sleep(){
System.out.println(name + "正在睡觉");
}
void bark(){
System.out.println(name + "汪汪汪~~~");
}
}
// Cat.java
public class Cat{
String name;
int age;
float weight;
public void eat(){
System.out.println(name + "正在吃饭");
}
public void sleep(){
System.out.println(name + "正在睡觉");
}
void mew(){
System.out.println(name + "喵喵喵~~~");
}
}
重复代码会导致维护成本升高(修改共性逻辑需在所有相关类中逐一操作)、代码冗余度高。面向对象思想提出的继承概念,专门用于抽取共性、实现代码复用,解决此类问题。
2.2 继承的核心概念
继承是面向对象程序设计中实现代码复用的最重要手段,允许程序员在保持原有类特性的基础上扩展新功能,产生新的类(称为子类 / 派生类),被继承的原有类称为父类 / 基类 / 超类。
继承的核心价值
- 代码复用:子类无需重复定义父类已有的成员(变量和方法),直接继承使用;
- 构建层次结构:呈现 “由简单到复杂” 的认知过程,如 “动物→狗→柯基犬”“电子设备→手机→智能手机”,使代码结构更清晰、符合现实逻辑。
继承关系示例
以动物、狗、猫的关系为例,继承后的类结构:
- 父类(Animal):包含共性成员(name、age、weight、eat ()、sleep ());
- 子类(Dog):继承 Animal 类,新增特有方法 bark ();
- 子类(Cat):继承 Animal 类,新增特有方法 mew ()。
这种结构下,子类既拥有父类的所有功能,又能通过新增成员体现自身特殊性,完美解决代码冗余问题。
2.3 继承的语法规则
Java 中通过extends关键字声明类之间的继承关系,语法格式如下:
修饰符 class 子类 extends 父类 {
// 子类特有成员(变量 + 方法)
}
基于继承的重构案例
针对 2.1 中的狗和猫案例,使用继承重构后的代码:
// 父类:Animal.java
public class Animal {
String name;
int age;
float weight;
public void eat() {
System.out.println(name + "正在吃饭");
}
public void sleep() {
System.out.println(name + "正在睡觉");
}
}
// 子类:Dog.java(继承Animal)
public class Dog extends Animal {
// 子类特有方法
void bark() {
System.out.println(name + "汪汪汪~~~");
}
}
// 子类:Cat.java(继承Animal)
public class Cat extends Animal {
// 子类特有方法
void mew() {
System.out.println(name + "喵喵喵~~~");
}
}
// 测试类:TestExtend.java
public class TestExtend {
public static void main(String[] args) {
Dog dog = new Dog();
// 访问父类继承的成员变量
dog.name = "小七";
dog.age = 2;
dog.weight = 10.5f;
// 访问父类继承的成员方法
dog.eat(); // 输出:小七正在吃饭
dog.sleep(); // 输出:小七正在睡觉
// 访问子类特有方法
dog.bark(); // 输出:小七汪汪汪~~~
Cat cat = new Cat();
cat.name = "元宝";
cat.age = 1;
cat.weight = 5.2f;
cat.eat(); // 输出:元宝正在吃饭
cat.sleep(); // 输出:元宝正在睡觉
cat.mew(); // 输出:元宝喵喵喵~~~
}
}
继承的关键注意事项
- 子类会继承父类中所有非
private修饰的成员变量和成员方法(private成员虽被继承,但无法直接访问); - 子类必须体现特殊性:子类继承父类后,需新增特有成员(变量或方法),否则继承无意义(若子类与父类完全一致,直接使用父类即可);
- Java 不支持多继承:一个子类只能有一个直接父类(单继承),但支持多层继承(如 A→B→C),避免多继承带来的 “菱形问题”(多个父类有同名方法时,子类无法确定调用哪个)。
2.4 父类成员的访问规则
子类继承父类的成员后,访问时遵循 “就近原则”—— 优先访问子类自身的成员,若子类没有,则向上追溯到父类。
2.4.1 访问父类的成员变量
根据子类与父类是否存在同名成员变量,分为两种情况:
情况 1:子类与父类无同名成员变量
子类可直接访问父类继承的成员变量,无需额外关键字:
// 父类
public class Base {
int a;
int b;
}
// 子类
public class Derived extends Base {
int c;
public void method() {
a = 10; // 直接访问父类继承的a
b = 20; // 直接访问父类继承的b
c = 30; // 访问子类自身的c
System.out.println(a + b + c); // 输出:60
}
}
情况 2:子类与父类有同名成员变量
当子类成员变量与父类成员变量同名时(无论类型是否相同),优先访问子类自身的成员变量;若需访问父类的同名成员变量,需使用super关键字:
// 父类
public class Base {
int a = 10;
int b = 20;
int c = 30;
}
// 子类
public class Derived extends Base {
int a = 100; // 与父类a同名且类型相同
char b = 'B'; // 与父类b同名但类型不同
public void method() {
// 优先访问子类自身成员
System.out.println(a); // 输出:100(子类的a)
System.out.println(b); // 输出:B(子类的b)
// 子类无c,访问父类继承的c
System.out.println(c); // 输出:30(父类的c)
// 访问父类的同名成员变量,需用super
System.out.println(super.a); // 输出:10(父类的a)
System.out.println(super.b); // 输出:20(父类的b)
}
}
2.4.2 访问父类的成员方法
与成员变量类似,成员方法的访问分为 “同名” 和 “不同名” 两种情况,且需区分 “重载” 和 “重写” 场景:
情况 1:子类与父类无同名成员方法
子类可直接访问自身方法,若子类无该方法,则访问父类继承的方法:
// 父类
public class Base {
public void methodA() {
System.out.println("Base中的methodA()");
}
}
// 子类
public class Derived extends Base {
public void methodB() {
System.out.println("Derived中的methodB()");
}
public void methodC() {
methodB(); // 访问子类自身的methodB()
methodA(); // 访问父类继承的methodA()
// methodD(); // 编译失败:整个继承体系中无methodD()
}
}
情况 2:子类与父类有同名成员方法
需区分两种核心场景:重载和重写(重写将在多态章节详细讲解):
- 重载场景:父类与子类的同名方法参数列表不同(方法名相同,参数个数 / 类型 / 顺序不同),根据调用时传递的参数类型选择对应的方法:
// 父类
public class Base {
public void methodA() {
System.out.println("Base中的methodA()(无参)");
}
}
// 子类
public class Derived extends Base {
// 与父类methodA()构成重载(参数列表不同)
public void methodA(int a) {
System.out.println("Derived中的methodA()(int参数:" + a + ")");
}
public void methodC() {
methodA(); // 无参,调用父类的methodA()
methodA(20); // 有int参数,调用子类的methodA(int)
}
}
- 重写场景:父类与子类的同名方法参数列表、返回值类型完全一致(或返回值为父子关系),此时子类方法会覆盖父类方法,直接调用时优先执行子类方法;若需调用父类的重写方法,需使用
super关键字:
// 父类
public class Base {
public void methodB() {
System.out.println("Base中的methodB()");
}
}
// 子类
public class Derived extends Base {
// 与父类methodB()构成重写(参数列表、返回值一致)
@Override
public void methodB() {
System.out.println("Derived中的methodB()");
}
public void methodC() {
methodB(); // 直接调用,执行子类的methodB()
super.methodB(); // 用super调用,执行父类的methodB()
}
}
2.5 super 关键字:访问父类成员的核心工具
super是 Java 中的关键字,核心作用是在子类中访问父类的成员(成员变量、成员方法)或调用父类的构造方法。它相当于子类对象中 “从父类继承下来的部分” 的引用,与this(当前对象引用)形成互补。
2.5.1 super 的核心用法
- 访问父类的成员变量:当子类与父类有同名成员变量时,用
super.变量名访问父类变量; - 访问父类的成员方法:当子类重写父类方法时,用
super.方法名(参数)调用父类的方法; - 调用父类的构造方法:在子类构造方法中,用
super(参数)调用父类的指定构造方法(必须是构造方法的第一条语句)。
2.5.2 super 的使用示例
// 父类
public class Base {
int a = 10;
public Base() {
System.out.println("Base的无参构造方法");
}
public Base(int a) {
this.a = a;
System.out.println("Base的有参构造方法(a=" + a + ")");
}
public void method() {
System.out.println("Base中的method()");
}
}
// 子类
public class Derived extends Base {
int a = 100;
public Derived() {
super(20); // 调用父类的有参构造方法(必须在第一条语句)
System.out.println("Derived的无参构造方法");
}
@Override
public void method() {
System.out.println("子类的a:" + a); // 访问子类自身的a
System.out.println("父类的a:" + super.a); // 访问父类的a
super.method(); // 调用父类的method()
}
public static void main(String[] args) {
Derived d = new Derived();
d.method();
}
}
运行结果:
Base的有参构造方法(a=20)
Derived的无参构造方法
子类的a:100
父类的a:20
Base中的method()
2.5.3 super 的注意事项
- 只能在非静态方法中使用:
super依赖于子类对象的创建,而静态方法不依赖对象,因此在静态方法中使用super会编译报错; - 构造方法中使用时需注意顺序:
super(参数)必须是子类构造方法的第一条语句,且不能与this(参数)(调用子类自身构造方法)同时存在; - 不能访问父类的
private成员:super只能访问父类中protected、public或默认访问权限(同一包)的成员,private成员虽被继承,但无法通过super直接访问。
2.6 子类构造方法的执行规则
继承体系中,子类对象的创建遵循 “先有父,再有子” 的逻辑 —— 子类对象的成员由两部分组成:父类继承的成员和子类新增的成员。因此,构造子类对象时,必须先调用父类的构造方法初始化父类成员,再执行子类自身的构造方法初始化子类成员。
2.6.1 默认情况下的构造方法调用
如果父类存在无参构造方法(显式定义或默认生成),子类构造方法中会隐含一条super()语句(编译器自动添加),用于调用父类的无参构造方法,且该语句必须是子类构造方法的第一条语句:
// 父类
public class Base {
public Base() {
System.out.println("Base的无参构造方法");
}
}
// 子类
public class Derived extends Base {
public Derived() {
// 编译器自动添加:super();
System.out.println("Derived的无参构造方法");
}
}
// 测试
public class Test {
public static void main(String[] args) {
Derived d = new Derived();
}
}
运行结果:
Base的无参构造方法
Derived的无参构造方法
2.6.2 父类无无参构造方法的情况
如果父类只定义了有参构造方法(未显式定义无参构造方法),编译器不会为父类生成默认的无参构造方法。此时子类必须在构造方法中显式调用父类的有参构造方法(用super(参数)),否则编译报错:
// 父类:只有有参构造方法
public class Base {
private int a;
public Base(int a) {
this.a = a;
System.out.println("Base的有参构造方法(a=" + a + ")");
}
}
// 子类:必须显式调用父类的有参构造方法
public class Derived extends Base {
public Derived() {
super(10); // 显式调用父类的有参构造方法
System.out.println("Derived的无参构造方法");
}
public Derived(int a, int b) {
super(a); // 显式调用父类的有参构造方法
System.out.println("Derived的有参构造方法(b=" + b + ")");
}
}
// 测试
public class Test {
public static void main(String[] args) {
Derived d1 = new Derived();
Derived d2 = new Derived(20, 30);
}
}
运行结果:
Base的有参构造方法(a=10)
Derived的无参构造方法
Base的有参构造方法(a=20)
Derived的有参构造方法(b=30)
2.6.3 子类构造方法的核心规则
- 子类构造方法中,必须调用父类的构造方法(显式或隐式);
- 若父类无无参构造方法,子类必须显式用
super(参数)调用父类的有参构造方法; super(参数)必须是子类构造方法的第一条语句,且只能出现一次;- 子类构造方法中,
super(参数)和this(参数)不能同时存在(两者都要求是第一条语句)。
2.7 super 与 this 的对比
super和this都是 Java 中的关键字,且都能用于访问成员变量、调用成员方法和构造方法,但核心定位和使用场景有明显区别:
| 对比维度 | this | super |
|---|---|---|
| 核心含义 | 当前对象的引用 | 子类对象中父类继承部分的引用 |
| 访问成员变量 | 优先访问子类自身的成员变量,若无则向上追溯 | 直接访问父类的成员变量 |
| 访问成员方法 | 优先调用子类自身的方法,若无则向上追溯 | 直接调用父类的方法 |
| 调用构造方法 | this(参数):调用子类自身的其他构造方法 | super(参数):调用父类的构造方法 |
| 构造方法中使用 | 可选,用户不写则无 | 必选,用户不写则编译器自动添加super() |
| 使用场景 | 区分子类自身与父类的同名成员,调用自身其他构造方法 | 访问父类的成员或构造方法,解决同名成员冲突 |
| 静态方法中 | 不能使用 | 不能使用 |
示例:super 与 this 的协同使用
// 父类
public class Base {
int value;
public Base(int value) {
this.value = value;
}
public void show() {
System.out.println("Base value: " + value);
}
}
// 子类
public class Derived extends Base {
int value;
// 调用子类自身的另一个构造方法
public Derived() {
this(100);
}
// 调用父类的构造方法
public Derived(int value) {
super(200); // 父类value=200
this.value = value; // 子类value=100
}
@Override
public void show() {
System.out.println("子类value: " + this.value); // 100
System.out.println("父类value: " + super.value); // 200
super.show(); // 调用父类的show():Base value: 200
}
public static void main(String[] args) {
Derived d = new Derived();
d.show();
}
}
运行结果:
子类value: 100
父类value: 200
Base value: 200
2.8 继承体系中的初始化顺序
类的初始化涉及静态代码块、实例代码块和构造方法,继承体系会让初始化顺序更复杂。掌握初始化顺序是理解对象创建过程的关键,也是面试高频考点。
2.8.1 无继承关系的初始化顺序
无继承关系时,初始化顺序为:
- 静态代码块(只执行一次,类加载时执行);
- 实例代码块(每次创建对象时执行);
- 构造方法(每次创建对象时执行,在实例代码块之后)。
示例:
class Person {
public String name;
public int age;
// 静态代码块
static {
System.out.println("Person: 静态代码块执行");
}
// 实例代码块
{
System.out.println("Person: 实例代码块执行");
}
// 构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Person: 构造方法执行");
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person("张三", 18);
System.out.println("====================");
Person p2 = new Person("李四", 20);
}
}
运行结果:
Person: 静态代码块执行
Person: 实例代码块执行
Person: 构造方法执行
====================
Person: 实例代码块执行
Person: 构造方法执行
2.8.2 有继承关系的初始化顺序
存在继承关系时,初始化顺序为:
- 父类静态代码块(最早执行,只执行一次);
- 子类静态代码块(在父类静态代码块之后,只执行一次);
- 父类实例代码块(每次创建子类对象时执行);
- 父类构造方法(在父类实例代码块之后);
- 子类实例代码块(在父类构造方法之后);
- 子类构造方法(最后执行)。
核心原则:静态代码块优先于实例相关代码,父类相关代码优先于子类相关代码。
示例:
// 父类
class Person {
public String name;
public int age;
static {
System.out.println("Person: 静态代码块执行");
}
{
System.out.println("Person: 实例代码块执行");
}
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Person: 构造方法执行");
}
}
// 子类
class Student extends Person {
public String studentId;
static {
System.out.println("Student: 静态代码块执行");
}
{
System.out.println("Student: 实例代码块执行");
}
public Student(String name, int age, String studentId) {
super(name, age); // 调用父类构造方法
this.studentId = studentId;
System.out.println("Student: 构造方法执行");
}
}
// 测试
public class Test {
public static void main(String[] args) {
Student s1 = new Student("张三", 18, "2023001");
System.out.println("====================");
Student s2 = new Student("李四", 20, "2023002");
}
}
运行结果:
Person: 静态代码块执行
Student: 静态代码块执行
Person: 实例代码块执行
Person: 构造方法执行
Student: 实例代码块执行
Student: 构造方法执行
====================
Person: 实例代码块执行
Person: 构造方法执行
Student: 实例代码块执行
Student: 构造方法执行
2.8.3 关键结论
- 静态代码块只在类加载时执行一次,无论创建多少对象,都不会重复执行;
- 实例代码块和构造方法每次创建对象时都会执行,且实例代码块始终在构造方法之前执行;
- 继承体系中,父类的初始化(静态 + 实例 + 构造)完全完成后,才会开始子类的初始化;
- 子类构造方法中的
super(...)会触发父类的实例代码块和构造方法执行,但不会重复触发父类的静态代码块(类加载只执行一次)。
2.9 protected 关键字:继承中的访问控制
访问限定符用于控制类或类成员的访问范围,protected是专门为继承设计的访问限定符 —— 允许子类访问父类的成员,同时限制非子类的外部访问,平衡封装性和继承性。
2.9.1 访问限定符的范围对比
Java 中 4 种访问限定符的访问范围:
| 访问范围 | private | default(默认,无修饰符) | protected | public |
|---|---|---|---|---|
| 同一包中的同一类 | ✔️ | ✔️ | ✔️ | ✔️ |
| 同一包中的不同类 | ❌ | ✔️ | ✔️ | ✔️ |
| 不同包中的子类 | ❌ | ❌ | ✔️ | ✔️ |
| 不同包中的非子类 | ❌ | ❌ | ❌ | ✔️ |
2.9.2 protected 在继承中的作用
protected的核心作用:允许不同包中的子类访问父类的成员,同时禁止不同包中的非子类访问。这让父类能安全地向子类暴露必要的成员,而不被其他无关类访问,保证封装性。
示例:不同包中的子类访问父类的 protected 成员
// 包1:com.example.parent
package com.example.parent;
public class Base {
private int a = 1; // 私有成员,任何外部类都无法访问
protected int b = 2; // protected成员,不同包子类可访问
int c = 3; // 默认访问权限,不同包无法访问
public int d = 4; // 公共成员,所有类可访问
}
// 包2:com.example.child(不同包)
package com.example.child;
import com.example.parent.Base;
// 不同包中的子类
public class Derived extends Base {
public void method() {
// System.out.println(a); // 编译失败:private成员不可访问
System.out.println(b); // 编译成功:protected成员可访问
// System.out.println(c); // 编译失败:默认权限,不同包不可访问
System.out.println(d); // 编译成功:public成员可访问
}
}
// 包2:com.example.other(不同包的非子类)
package com.example.other;
import com.example.parent.Base;
public class Test {
public static void main(String[] args) {
Base base = new Base();
// System.out.println(base.a); // 编译失败
// System.out.println(base.b); // 编译失败:不同包非子类不可访问
// System.out.println(base.c); // 编译失败
System.out.println(base.d); // 编译成功
}
}
2.9.3 访问权限的选择原则
实际开发中,选择访问权限的核心原则是 “最小权限原则”—— 尽可能使用更严格的访问权限,隐藏内部实现细节,只暴露必要的接口给外部,提高代码的安全性和可维护性。
具体选择建议:
- 成员变量:优先使用
private,通过getter/setter方法暴露必要的访问; - 成员方法:
- 仅类内部使用:
private; - 同一包中的类使用:
default; - 子类需要继承使用:
protected; - 外部类(任何包)都需要使用:
public;
- 仅类内部使用:
- 避免滥用
public:不要将所有成员都设为public,否则会破坏封装性,导致代码难以维护。
2.10 Java 的继承方式
Java 中支持的继承方式有三种:单继承、多层继承,不支持多继承。
2.10.1 单继承
一个子类只能有一个直接父类,这是 Java 的基本继承规则,避免多继承带来的歧义问题。
语法示例:
public class A {}
public class B extends A {} // 正确:B的直接父类是A
2.10.2 多层继承
子类可以继承自另一个子类,形成 “祖父→父→子” 的多层继承结构。多层继承可进一步抽取共性,构建更精细的层次结构,但继承层次不宜过深(建议不超过 3 层),否则会导致代码耦合度高、难以维护。
语法示例:
public class A {}
public class B extends A {}
public class C extends B {} // 正确:C的直接父类是B,间接父类是A
2.10.3 多继承(不支持)
Java 不允许一个子类同时继承多个直接父类,即 “多继承” 非法。例如:
public class A {}
public class B {}
// 编译失败:Java不支持多继承
public class C extends A, B {}
为什么不支持多继承?多继承会导致 “菱形问题”(钻石问题):假设类 C 同时继承类 A 和类 B,而 A 和 B 都有一个同名的方法method(),那么当 C 调用method()时,无法确定是调用 A 的方法还是 B 的方法,引发歧义。
例如:
class A {
public void method() {
System.out.println("A的method()");
}
}
class B {
public void method() {
System.out.println("B的method()");
}
}
// 若支持多继承,C调用method()时会产生歧义
class C extends A, B {}
public class Test {
public static void main(String[] args) {
C c = new C();
c.method(); // 到底调用A的还是B的?
}
}
为解决多继承的需求,Java 提供了接口(interface)机制 —— 一个类可以实现多个接口,接口中只定义方法签名,不包含实现,从而避免歧义。
2.11 final 关键字:限制继承与修改
final是 Java 中的关键字,可用于修饰类、成员方法和成员变量,核心作用是 “限制修改”,在继承体系中常用于禁止类被继承或方法被重写。
2.11.1 修饰类:禁止类被继承
用final修饰的类称为 “最终类”,不能被其他类继承。Java 中的String类就是典型的最终类,保证了字符串的不可变性和安全性。
语法示例:
// 最终类:不能被继承
final public class Animal {}
// 编译失败:无法继承最终类
public class Dog extends Animal {}
2.11.2 修饰方法:禁止方法被重写
用final修饰的成员方法,子类不能对其进行重写(但可以继承使用)。这常用于父类的核心方法,避免子类修改其实现逻辑,保证程序的稳定性。
语法示例:
public class Base {
// final方法:不能被重写
public final void method() {
System.out.println("Base的final方法");
}
}
public class Derived extends Base {
// 编译失败:无法重写final方法
@Override
public void method() {
System.out.println("尝试重写final方法");
}
}
2.11.3 修饰变量:定义常量
用final修饰的变量(成员变量或局部变量)称为 “常量”,一旦赋值后不能再修改。常量的命名通常采用 “大写字母 + 下划线” 的格式(如MAX_VALUE)。
语法示例:
public class ConstantDemo {
// 成员常量:必须在声明时或构造方法中初始化
public static final int MAX_AGE = 120;
public final String NAME;
public ConstantDemo(String name) {
this.NAME = name; // 构造方法中初始化常量
}
public static void main(String[] args) {
ConstantDemo demo = new ConstantDemo("张三");
// demo.NAME = "李四"; // 编译失败:常量不能修改
// ConstantDemo.MAX_AGE = 150; // 编译失败:静态常量不能修改
}
}
2.11.4 final 的核心使用场景
- 保护核心类不被继承(如
String、工具类); - 保护核心方法不被重写(如父类的关键业务逻辑方法);
- 定义不可修改的常量(如配置参数、固定值);
- 局部变量修饰:避免方法内部变量被意外修改,提高代码可读性。
2.12 继承与组合:代码复用的两种选择
除了继承,组合也是 Java 中实现代码复用的重要方式。组合是指将一个类的实例作为另一个类的成员变量,通过调用该实例的方法来复用代码,体现 “has-a”(有一个)的关系;而继承体现 “is-a”(是一个)的关系。
2.12.1 继承与组合的对比
| 特性 | 继承(is-a) | 组合(has-a) |
|---|---|---|
| 关系本质 | 子类是父类的一种特殊类型 | 类包含另一个类的实例 |
| 代码复用方式 | 子类直接继承父类的成员 | 通过成员对象调用其方法 |
| 耦合度 | 高(子类依赖父类的实现) | 低(仅依赖成员对象的接口) |
| 灵活性 | 低(继承层次固定,难以修改) | 高(可动态替换成员对象) |
| 扩展性 | 差(父类修改可能影响子类) | 好(成员对象可独立扩展) |
| 适用场景 | 子类与父类存在明确的 “is-a” 关系 | 类之间是 “has-a” 的组合关系 |
2.12.2 组合的实现示例
以 “汽车” 为例,汽车由轮胎、发动机、车载系统等部件组成,这种关系适合用组合实现:
// 轮胎类
class Tire {
public void rotate() {
System.out.println("轮胎旋转,汽车前进");
}
}
// 发动机类
class Engine {
public void start() {
System.out.println("发动机启动,提供动力");
}
}
// 车载系统类
class VehicleSystem {
public void navigation() {
System.out.println("车载系统导航:前往目的地");
}
}
// 汽车类:组合轮胎、发动机、车载系统
class Car {
// 组合的成员对象
private Tire tire = new Tire();
private Engine engine = new Engine();
private VehicleSystem vs = new VehicleSystem();
// 汽车的方法通过调用成员对象的方法实现
public void start() {
engine.start();
tire.rotate();
System.out.println("汽车启动成功");
}
public void navigate() {
vs.navigation();
}
}
// 奔驰车类:继承Car,新增特有功能
class Benz extends Car {
public void autoDrive() {
System.out.println("奔驰车自动驾驶功能启动");
}
}
// 测试
public class Test {
public static void main(String[] args) {
Benz benz = new Benz();
benz.start(); // 复用Car的组合功能
benz.navigate(); // 复用车载系统功能
benz.autoDrive(); // 子类特有功能
}
}
运行结果:
发动机启动,提供动力
轮胎旋转,汽车前进
汽车启动成功
车载系统导航:前往目的地
奔驰车自动驾驶功能启动
2.12.3 继承与组合的选择原则
实际开发中,选择继承还是组合,核心在于判断类之间的关系:
- 若类之间存在明确的 “is-a” 关系(如狗是动物、奔驰是汽车),且需要复用父类的大部分成员,同时子类需要重写父类的部分方法,则使用继承;
- 若类之间是 “has-a” 的组合关系(如汽车有发动机、人有手机),且希望降低代码耦合度、提高灵活性,则使用组合;
- 优先使用组合:组合的耦合度更低,扩展性更强,更符合 “合成复用原则”(面向对象设计原则之一),即 “尽量使用组合,而非继承来实现代码复用”。
例如,实现 “鸟会飞” 的功能:
- 错误方式:定义
Bird类继承Flyable类(Flyable类包含fly()方法),但后续添加 “飞机也会飞” 时,飞机显然不是鸟,继承关系不成立; - 正确方式:定义
Flyable接口(包含fly()方法),Bird类和Plane类分别实现Flyable接口,同时Bird类组合Wing(翅膀)类,通过翅膀的flap()方法实现fly(),既复用了代码,又避免了继承的局限性。
三、多态(Polymorphism)
3.1 多态的核心概念
多态是面向对象编程的核心特性之一,通俗来说,就是 “同一行为,不同对象表现出不同的结果”。具体而言,当不同的对象执行同一个方法时,会根据对象的实际类型,调用该类型对应的方法实现,从而产生不同的执行效果。
3.1.1 生活中的多态示例
- 打印机打印:彩色打印机打印出彩色文档,黑白打印机打印出黑白文档,“打印” 是同一行为,但不同打印机的表现不同;
- 动物进食:猫吃鱼、狗吃骨头、羊吃草,“进食” 是同一行为,但不同动物的食物和方式不同;
- 电子设备充电:手机用 Type-C 接口充电,笔记本用 PD 接口充电,“充电” 是同一行为,但不同设备的充电方式不同。
3.1.2 Java 中的多态示例
以动物进食为例,Java 中通过继承、重写和父类引用指向子类对象实现多态:
// 父类:Animal
public class Animal {
String name;
int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
// 父类的方法:进食
public void eat() {
System.out.println(name + "正在吃食物");
}
}
// 子类:Cat(重写eat()方法)
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "正在吃鱼~~~");
}
}
// 子类:Dog(重写eat()方法)
public class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + "正在吃骨头~~~");
}
}
// 测试类
public class TestPolymorphism {
// 父类引用作为参数,接收任意子类对象
public static void feed(Animal animal) {
animal.eat(); // 同一方法调用,不同对象表现不同
}
public static void main(String[] args) {
Animal cat = new Cat("元宝", 2);
Animal dog = new Dog("小七", 1);
feed(cat); // 输出:元宝正在吃鱼~~~
feed(dog); // 输出:小七正在吃骨头~~~
}
}
在上述示例中,feed()方法的参数是父类Animal类型,但实际传入的是Cat和Dog的对象。当调用animal.eat()时,程序会根据animal引用的实际对象类型,调用对应的eat()方法实现 —— 这就是多态的核心体现。
3.2 多态的实现条件
Java 中实现多态必须满足三个条件,缺一不可:
- 继承体系:子类必须继承自父类(或实现接口,接口多态本质是继承的延伸);
- 方法重写:子类必须重写父类中的方法(或接口中的抽象方法);
- 父类引用指向子类对象:通过父类类型的引用变量,指向子类类型的对象(即 “向上转型”)。
3.2.1 条件详解
- 继承体系:多态建立在继承基础上,没有继承就没有多态。子类通过继承获得父类的方法,为后续重写提供基础;
- 方法重写:重写是多态的核心 —— 如果子类不重写父类方法,那么父类引用调用的始终是父类的方法,无法体现 “不同对象不同表现”;
- 父类引用指向子类对象:这是多态的触发条件。只有当父类引用指向子类对象时,编译器无法确定调用的是父类还是子类的方法,需要在运行时根据实际对象类型动态绑定方法。
3.2.2 反例:缺少任一条件则无法实现多态
- 缺少方法重写:
class Animal {
public void eat() {
System.out.println("吃食物");
}
}
class Cat extends Animal {
// 未重写eat()方法
}
public class Test {
public static void main(String[] args) {
Animal cat = new Cat();
cat.eat(); // 输出:吃食物(调用父类方法,无多态)
}
}
- 缺少父类引用指向子类对象:
class Animal {
public void eat() {
System.out.println("吃食物");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("吃鱼");
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat(); // 输出:吃鱼(直接调用子类方法,无多态)
}
}
3.3 重写(Override):多态的核心实现手段
重写(Override)又称 “覆盖”,是子类对父类中可继承的方法(非private、非static、非final、非构造方法)的实现逻辑进行重新编写,使子类方法的行为更符合自身特性。重写是实现多态的关键,没有重写就没有多态。
3.3.1 重写的规则
重写必须严格遵循以下规则,否则编译报错或无法构成有效重写:
- 方法原型一致:子类方法的方法名、参数列表(个数、类型、顺序)必须与父类方法完全一致;
- 返回值类型兼容:子类方法的返回值类型必须与父类方法的返回值类型相同,或为父类返回值类型的子类(即 “协变返回类型”);
- 访问权限不严格:子类方法的访问权限不能比父类方法的访问权限更严格(可以更宽松)。例如:
- 父类方法为
public,子类方法不能为protected、default或private; - 父类方法为
protected,子类方法可以为public或protected(不能为default或private);
- 父类方法为
- 不能重写的方法:
static方法:静态方法属于类,不属于对象,无法重写(子类可定义同名静态方法,但这是隐藏父类方法,而非重写);private方法:私有方法只能在父类内部访问,子类无法继承,因此不能重写;final方法:最终方法禁止被重写;- 构造方法:构造方法是类特有的,不能被重写;
- 注解显式声明:子类重写方法时,建议添加
@Override注解。该注解用于告诉编译器 “此方法是重写父类的方法”,编译器会自动校验重写规则,若不符合则编译报错(例如方法名拼写错误、参数列表不一致)。
3.3.2 重写的示例
// 父类
class Animal {
public Animal eat() {
System.out.println("动物吃食物");
return this;
}
protected void sleep() {
System.out.println("动物睡觉");
}
public static void staticMethod() {
System.out.println("父类静态方法");
}
private void privateMethod() {
System.out.println("父类私有方法");
}
}
// 子类
class Cat extends Animal {
// 重写eat():返回值为Cat(父类返回值Animal的子类,协变返回类型)
@Override
public Cat eat() {
System.out.println("猫吃鱼");
return this;
}
// 重写sleep():访问权限从protected改为public(更宽松)
@Override
public void sleep() {
System.out.println("猫蜷缩着睡觉");
}
// 不是重写:静态方法不能重写,只是隐藏父类方法
public static void staticMethod() {
System.out.println("子类静态方法");
}
// 不是重写:父类private方法无法继承
private void privateMethod() {
System.out.println("子类私有方法");
}
}
// 测试
public class TestOverride {
public static void main(String[] args) {
Animal cat = new Cat();
cat.eat(); // 输出:猫吃鱼(重写生效)
cat.sleep(); // 输出:猫蜷缩着睡觉(重写生效)
Animal.staticMethod(); // 输出:父类静态方法(静态方法属于类)
Cat.staticMethod(); // 输出:子类静态方法(隐藏父类方法)
}
}
3.3.3 重写与重载的区别
重写(Override)和重载(Overload)是 Java 中两个容易混淆的概念,二者的核心区别如下:
| 对比维度 | 重写(Override) | 重载(Overload) |
|---|---|---|
| 定义 | 子类重写父类的方法 | 同一类中定义多个同名方法 |
| 适用范围 | 继承体系(子类与父类) | 同一类(或子类继承父类方法后重载) |
| 方法名 | 必须相同 | 必须相同 |
| 参数列表 | 必须相同(个数、类型、顺序) | 必须不同(个数、类型、顺序) |
| 返回值类型 | 相同或为父类返回值的子类 | 可以任意修改 |
| 访问权限 | 不能更严格(可更宽松) | 可以任意修改 |
| 静态方法 | 不能重写 | 可以重载 |
| 与多态的关系 | 是多态的核心实现手段 | 是类的多态性表现(编译时多态) |
| 绑定时机 | 运行时绑定(动态绑定) | 编译时绑定(静态绑定) |
示例:重写与重载的对比
class Calculator {
// 重载:同一类中同名方法,参数列表不同
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
class AdvancedCalculator extends Calculator {
// 重写:子类重写父类的add(int, int)方法
@Override
public int add(int a, int b) {
System.out.println("高级计算器计算整数加法");
return a + b + 1; // 新增逻辑:加1
}
// 重载:子类中重载add方法(参数列表不同)
public long add(long a, long b) {
return a + b;
}
}
public class TestOverrideOverload {
public static void main(String[] args) {
Calculator calc = new AdvancedCalculator();
// 重写生效:调用子类的add(int, int)
System.out.println(calc.add(1, 2)); // 输出:高级计算器计算整数加法 → 4
// 重载:调用父类的add(double, double)
System.out.println(calc.add(1.5, 2.5)); // 输出:4.0
AdvancedCalculator advCalc = new AdvancedCalculator();
// 重载:调用子类的add(long, long)
System.out.println(advCalc.add(10L, 20L)); // 输出:30
}
}
3.4 动态绑定与静态绑定:多态的底层原理
多态的实现依赖于 Java 的 “绑定机制”—— 绑定是指将方法调用与方法实现关联起来的过程。根据绑定发生的时机,可分为静态绑定和动态绑定。
3.4.1 静态绑定(前期绑定 / 早绑定)
静态绑定是指在编译时就确定方法调用与方法实现的关联,即方法调用的目标在编译阶段就已明确。
典型场景:
- 方法重载:编译器根据方法的参数列表(类型、个数、顺序)确定调用哪个重载方法;
- 静态方法调用:静态方法属于类,调用时直接关联类的方法实现;
private方法、final方法调用:这些方法无法被重写,编译器可直接确定其实现。
示例:静态绑定(方法重载)
class MathUtil {
public static int multiply(int a, int b) {
return a * b;
}
public static double multiply(double a, double b) {
return a * b;
}
}
public class TestStaticBinding {
public static void main(String[] args) {
// 编译时确定调用multiply(int, int)
System.out.println(MathUtil.multiply(2, 3));
// 编译时确定调用multiply(double, double)
System.out.println(MathUtil.multiply(2.5, 3.5));
}
}
3.4.2 动态绑定(后期绑定 / 晚绑定)
动态绑定是指在运行时才确定方法调用与方法实现的关联,即方法调用的目标在编译阶段无法确定,需根据对象的实际类型动态选择。
典型场景:多态中的方法重写 —— 父类引用指向子类对象时,调用重写方法会触发动态绑定。
动态绑定的执行流程:
- 编译器检查父类中是否存在该方法(若不存在则编译报错);
- 运行时,JVM 获取当前对象的实际类型(子类类型);
- JVM 在子类中查找该方法的重写实现,若找到则调用;若未找到,则向上追溯到父类,调用父类的方法实现。
示例:动态绑定(多态重写)
class Animal {
public void eat() {
System.out.println("动物吃食物");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
public class TestDynamicBinding {
public static void main(String[] args) {
Animal animal;
// 运行时确定animal的实际类型是Cat,调用Cat的eat()
animal = new Cat();
animal.eat(); // 输出:猫吃鱼
// 运行时确定animal的实际类型是Dog,调用Dog的eat()
animal = new Dog();
animal.eat(); // 输出:狗吃骨头
}
}
3.4.3 动态绑定的底层实现
Java 中动态绑定的底层依赖于 “方法表”(Method Table)机制:
- 每个类在加载时,JVM 会为其生成一个方法表,包含该类的所有方法(包括继承自父类的方法);
- 子类的方法表中,会覆盖父类方法表中同名、同参数列表的方法条目,指向子类的重写实现;
- 当通过父类引用调用方法时,JVM 会先获取对象的实际类型,然后查找该类型的方法表,找到对应的方法实现并调用。
方法表机制的优势是提高动态绑定的效率 —— 无需每次调用都遍历继承体系查找方法,只需直接查询方法表即可。
3.5 向上转型与向下转型:多态中的类型转换
在多态中,父类引用指向子类对象的过程称为 “向上转型”,而将父类引用还原为子类对象的过程称为 “向下转型”。类型转换是多态的重要组成部分,正确使用类型转换可以灵活操作子类对象。
3.5.1 向上转型(Upcasting)
向上转型是指将子类类型的对象赋值给父类类型的引用变量,语法格式为:
父类类型 引用变量名 = new 子类类型();
例如:
Animal animal = new Cat(); // 向上转型:Cat对象→Animal引用
向上转型的本质
向上转型是 “从小范围类型向大范围类型” 的转换(子类是父类的特殊类型,范围更小),例如 “猫是动物的一种”,因此转换是安全的,编译器不会报错。
向上转型的使用场景
- 直接赋值:将子类对象直接赋值给父类引用;
- 方法传参:父类类型作为方法参数,接收任意子类对象(多态的核心场景);
- 方法返回值:方法返回值为父类类型,可返回任意子类对象。
示例:向上转型的三种场景
class Animal {
public void eat() {
System.out.println("吃食物");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("吃鱼");
}
public void mew() {
System.out.println("喵喵叫");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("吃骨头");
}
public void bark() {
System.out.println("汪汪叫");
}
}
public class TestUpcasting {
// 场景2:方法传参(父类类型接收子类对象)
public static void feed(Animal animal) {
animal.eat();
}
// 场景3:方法返回值(父类类型返回子类对象)
public static Animal getAnimal(String type) {
if ("cat".equals(type)) {
return new Cat(); // 返回Cat对象,向上转型为Animal
} else if ("dog".equals(type)) {
return new Dog(); // 返回Dog对象,向上转型为Animal
}
return null;
}
public static void main(String[] args) {
// 场景1:直接赋值
Animal cat = new Cat();
Animal dog = new Dog();
feed(cat); // 输出:吃鱼
feed(dog); // 输出:吃骨头
Animal animal1 = getAnimal("cat");
animal1.eat(); // 输出:吃鱼
Animal animal2 = getAnimal("dog");
animal2.eat(); // 输出:吃骨头
}
}
向上转型的优缺点
- 优点:简化代码,提高灵活性和扩展性。例如
feed()方法只需定义一个父类参数,即可接收所有子类对象,无需为每个子类编写单独的方法; - 缺点:父类引用无法访问子类特有的成员(变量和方法)。例如上述示例中,
animal引用无法调用Cat的mew()方法或Dog的bark()方法,因为编译器会将animal视为Animal类型,而Animal类中没有这些方法。
3.5.2 向下转型(Downcasting)
向下转型是指将父类类型的引用变量还原为子类类型的对象,语法格式为:
子类类型 引用变量名 = (子类类型) 父类引用变量;
例如:
Animal animal = new Cat(); // 向上转型
Cat cat = (Cat) animal; // 向下转型:Animal引用→Cat对象
向下转型的本质
向下转型是 “从大范围类型向小范围类型” 的转换,转换本身是不安全的 —— 因为父类引用可能指向的是其他子类对象,而非当前要转换的子类对象。
例如:
Animal animal = new Dog(); // 向上转型:Dog对象→Animal引用
// 编译通过,但运行时抛出ClassCastException(类型转换异常)
Cat cat = (Cat) animal;
上述代码中,animal实际指向的是Dog对象,试图将其转换为Cat类型,显然不合理,因此运行时会抛出异常。
向下转型的安全实现:instanceof 关键字
为避免向下转型时的类型转换异常,Java 提供了instanceof关键字,用于判断一个对象是否为某个类(或接口)的实例。语法格式为:
对象 instanceof 类名/接口名
若对象是该类(或其子类)的实例,则返回true;否则返回false。
安全的向下转型流程:
- 用
instanceof判断父类引用指向的对象是否为目标子类的实例; - 若返回
true,则进行向下转型; - 若返回
false,则不进行转型,避免异常。
示例:安全的向下转型
public class TestDowncasting {
public static void main(String[] args) {
Animal animal1 = new Cat();
Animal animal2 = new Dog();
// 安全向下转型:判断animal1是否为Cat实例
if (animal1 instanceof Cat) {
Cat cat = (Cat) animal1;
cat.mew(); // 输出:喵喵叫(成功调用子类特有方法)
}
// 安全向下转型:判断animal2是否为Dog实例
if (animal2 instanceof Dog) {
Dog dog = (Dog) animal2;
dog.bark(); // 输出:汪汪叫(成功调用子类特有方法)
}
// 避免错误转型:判断animal2是否为Cat实例
if (animal2 instanceof Cat) {
Cat cat = (Cat) animal2;
cat.mew();
} else {
System.out.println("animal2不是Cat实例,无法转型");
}
}
}
运行结果:
喵喵叫
汪汪叫
animal2不是Cat实例,无法转型
向下转型的使用场景
向下转型的主要场景是:当通过向上转型的父类引用访问子类特有的成员时,需要将父类引用还原为子类对象。例如,在多态中,若需要调用子类特有的方法(父类中没有的方法),就必须进行向下转型。
3.6 多态的优缺点
多态是面向对象编程的核心特性,合理使用多态可以显著提升代码质量,但同时也存在一些局限性。
3.6.1 多态的优点
- 降低代码耦合度,减少冗余代码:通过父类引用统一操作子类对象,避免编写大量重复的分支判断(如
if-else、switch-case),降低代码的 “圈复杂度”。
示例:无多态 vs 有多态
// 无多态:需要大量if-else判断
class Shape {
public void draw() {
System.out.println("画图形");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("♦");
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("●");
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("❀");
}
}
// 无多态的实现:需要判断每个形状类型
public class TestWithoutPolymorphism {
public static void drawShape(String shapeType) {
if ("rect".equals(shapeType)) {
new Rect().draw();
} else if ("cycle".equals(shapeType)) {
new Cycle().draw();
} else if ("flower".equals(shapeType)) {
new Flower().draw();
}
}
public static void main(String[] args) {
drawShape("rect");
drawShape("cycle");
drawShape("flower");
}
}
// 有多态的实现:无需判断,直接调用
public class TestWithPolymorphism {
public static void drawShape(Shape shape) {
shape.draw(); // 多态调用,自动匹配对应形状的draw()方法
}
public static void main(String[] args) {
drawShape(new Rect());
drawShape(new Cycle());
drawShape(new Flower());
}
}
对比可知,多态版本的代码更简洁、耦合度更低,当新增形状(如三角形)时,无需修改drawShape()方法,只需新增Triangle类并实现draw()方法即可,扩展性极强。
-
提高代码的可扩展性和可维护性:新增子类时,无需修改原有代码(如父类、测试类),只需保证子类符合父类的方法约定(重写父类方法),即可无缝集成到现有系统中,符合 “开闭原则”(对扩展开放,对修改关闭)。
-
提高代码的灵活性:父类引用可以指向任意子类对象,同一方法调用可根据对象类型动态切换行为,让代码更灵活、更通用。
3.6.2 多态的缺点
-
代码运行效率降低:多态依赖动态绑定,而动态绑定需要在运行时确定方法调用的目标,相比静态绑定(如重载、静态方法调用),会增加一定的运行时开销,降低代码执行效率(但这种开销在大多数场景下可忽略不计)。
-
属性没有多态性:多态仅适用于成员方法,不适用于成员变量。当父类和子类存在同名成员变量时,通过父类引用访问的始终是父类的成员变量,而非子类的成员变量。
示例:属性无多态性
class Animal {
String name = "动物";
}
class Cat extends Animal {
String name = "猫";
}
public class TestPolymorphismField {
public static void main(String[] args) {
Animal animal = new Cat();
System.out.println(animal.name); // 输出:动物(访问父类成员变量)
}
}
-
构造方法没有多态性:构造方法是类特有的,不能被重写,因此无法通过多态调用子类的构造方法。
-
无法直接访问子类特有成员:向上转型后的父类引用无法直接调用子类特有的方法和成员变量,必须通过向下转型才能访问,增加了代码的复杂性。
3.7 多态的常见陷阱与避坑指南
在使用多态时,若不注意一些细节,容易引发隐藏的错误。以下是多态的常见陷阱及避坑方法:
3.7.1 陷阱 1:在构造方法中调用重写的方法
在父类的构造方法中调用被子类重写的方法,会触发动态绑定,导致子类方法在子类对象初始化完成前被调用,可能引发成员变量未初始化的问题。
示例:构造方法中调用重写方法的陷阱
class B {
public B() {
func(); // 父类构造方法中调用func(),该方法被子类重写
}
public void func() {
System.out.println("B的func()");
}
}
class D extends B {
private int num = 1; // 子类成员变量
@Override
public void func() {
// 此时子类对象未初始化完成,num的值为默认值0
System.out.println("D的func(),num=" + num);
}
}
public class TestTrap1 {
public static void main(String[] args) {
D d = new D(); // 输出:D的func(),num=0
}
}
陷阱分析:
- 创建
D对象时,先调用父类B的构造方法; - 父类
B的构造方法调用func(),由于func()被D重写,动态绑定会调用D的func(); - 此时
D的构造方法尚未执行,成员变量num未初始化(默认值为 0),因此输出num=0,而非预期的1。
避坑指南:
- 尽量避免在构造方法中调用实例方法(尤其是可能被重写的方法);
- 若必须在构造方法中调用方法,优先调用
private或final方法(这些方法无法被重写,不会触发动态绑定)。
3.7.2 陷阱 2:混淆静态方法与实例方法的多态性
静态方法属于类,不属于对象,因此静态方法没有多态性。子类中定义与父类同名的静态方法,只是 “隐藏” 父类的静态方法,而非重写。
示例:静态方法无多态性
class Animal {
public static void staticMethod() {
System.out.println("Animal的静态方法");
}
}
class Cat extends Animal {
// 隐藏父类静态方法,不是重写
public static void staticMethod() {
System.out.println("Cat的静态方法");
}
}
public class TestTrap2 {
public static void main(String[] args) {
Animal animal = new Cat();
animal.staticMethod(); // 输出:Animal的静态方法(静态方法属于类,与对象无关)
Cat.staticMethod(); // 输出:Cat的静态方法(直接调用子类静态方法)
}
}
避坑指南:
- 明确静态方法无多态性,调用静态方法时,优先通过类名直接调用,而非对象引用;
- 避免在子类中定义与父类同名的静态方法,以免造成混淆。
3.7.3 陷阱 3:向下转型未做类型判断
未使用instanceof判断就进行向下转型,可能导致ClassCastException(类型转换异常),尤其在复杂的继承体系中,这种错误难以排查。
示例:未判断类型的向下转型陷阱
class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}
public class TestTrap3 {
public static void main(String[] args) {
Animal animal = new Dog();
// 未判断类型,直接转型,运行时抛出ClassCastException
Cat cat = (Cat) animal;
}
}
避坑指南:
- 所有向下转型都必须先通过
instanceof判断对象的实际类型; - 若转型失败会影响程序流程,可在
else分支中添加异常处理或日志输出,便于问题排查。
3.7.4 陷阱 4:忽略访问权限对重写的影响
子类重写父类方法时,访问权限不能比父类更严格,否则会编译报错,或无法构成有效重写。
示例:访问权限错误的重写
class Animal {
public void eat() {
System.out.println("吃食物");
}
}
class Cat extends Animal {
// 编译失败:子类方法访问权限(protected)比父类(public)更严格
@Override
protected void eat() {
System.out.println("吃鱼");
}
}
避坑指南:
- 重写方法时,确保子类方法的访问权限≥父类方法的访问权限;
- 建议父类方法使用
public或protected权限,子类重写时保持权限一致或更宽松。
3.8 多态的实际应用场景
多态在实际开发中应用广泛,以下是几个典型场景:
3.8.1 场景 1:框架中的统一接口设计
许多 Java 框架(如 Spring、MyBatis)都大量使用多态来设计统一接口。例如,Spring 的BeanFactory接口定义了获取 Bean 的统一方法,不同的实现类(如DefaultListableBeanFactory、AnnotationConfigApplicationContext)根据不同的场景提供具体实现,用户只需面向BeanFactory接口编程,无需关心具体实现类,大大提高了框架的灵活性和可扩展性。
3.8.2 场景 2:业务逻辑中的策略模式
策略模式是一种常用的设计模式,其核心思想是将不同的算法(策略)封装成独立的类,通过多态动态切换算法。例如,电商平台的优惠券抵扣策略(满减、折扣、无门槛券),可设计一个CouponStrategy接口,不同的优惠券类型对应不同的实现类,结算时根据用户选择的优惠券类型,动态调用对应的抵扣方法。
示例:策略模式中的多态应用
// 优惠券策略接口(父类)
interface CouponStrategy {
double calculateDiscount(double totalPrice);
}
// 满减策略(子类)
class FullReductionStrategy implements CouponStrategy {
private double fullPrice;
private double reducePrice;
public FullReductionStrategy(double fullPrice, double reducePrice) {
this.fullPrice = fullPrice;
this.reducePrice = reducePrice;
}
@Override
public double calculateDiscount(double totalPrice) {
if (totalPrice >= fullPrice) {
return totalPrice - reducePrice;
}
return totalPrice;
总结
以上就是今天要讲的内容,本文简单记录了JAVA学习笔记,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!

1407

被折叠的 条评论
为什么被折叠?



