JAVA学习 DAY12 继承和多态【万字长文!一篇搞定!】

 本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。

点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励! 

本文篇幅较长,建议先收藏再食用!


 系列文章目录

JAVA学习 DAY1 初识JAVA

JAVA学习 DAY2 java程序运行、注意事项、转义字符

JAVA学习 DAY3 注释与编码规范讲解

JAVA学习 DAY4 DOS操作讲解及实例

JAVA学习 DAY5 变量&数据类型 [万字长文!一篇搞定!] 

JAVA学习 DAY6 运算符

JAVA学习 DAY7 程序逻辑控制【万字长文!一篇搞定!】

JAVA学习 DAY8 方法【万字长文!一篇搞定!】

JAVA学习 DAY9 数组【万字长文!一篇搞定!】

JAVA学习 DAY10 类和对象【万字长文!一篇搞定!】

JAVA学习 DAY11 类和对象_续1【万字长文!一篇搞定!】

JAVA学习 DAY12 继承和多态【万字长文!一篇搞定!】

JAVA学习 DAY13 抽象类和接口【万字长文!一篇搞定!】

深度剖析 Java 图书管理系统设计与实现:类、接口与对象的实战应用

JAVA学习 DAY15 Java String类

JAVA学习 DAY16 Java 异常

Java 基础全攻略:从语法到实战项目(简单总结)


 拓展文章

Sublime安装指导!只需四步!

图文详解汉诺塔问题:从递归思想到代码实现(零基础也能看懂)

Java避坑指南:千万别在构造方法中调用重写的方法!(附代码案例+执行流程全解析)

Java 接口学习核心难点深度解析

深入剖析 Java 中的深拷贝与浅拷贝:原理、实现与最佳实践

目录

 系列文章目录

前言

一、章节核心目标

二、继承(Inheritance)

2.1 为什么需要继承

2.2 继承的核心概念

继承的核心价值

继承关系示例

2.3 继承的语法规则

基于继承的重构案例

继承的关键注意事项

2.4 父类成员的访问规则

2.4.1 访问父类的成员变量

情况 1:子类与父类无同名成员变量

情况 2:子类与父类有同名成员变量

2.4.2 访问父类的成员方法

情况 1:子类与父类无同名成员方法

情况 2:子类与父类有同名成员方法

2.5 super 关键字:访问父类成员的核心工具

2.5.1 super 的核心用法

2.5.2 super 的使用示例

2.5.3 super 的注意事项

2.6 子类构造方法的执行规则

2.6.1 默认情况下的构造方法调用

2.6.2 父类无无参构造方法的情况

2.6.3 子类构造方法的核心规则

2.7 super 与 this 的对比

示例:super 与 this 的协同使用

2.8 继承体系中的初始化顺序

2.8.1 无继承关系的初始化顺序

2.8.2 有继承关系的初始化顺序

2.8.3 关键结论

2.9 protected 关键字:继承中的访问控制

2.9.1 访问限定符的范围对比

2.9.2 protected 在继承中的作用

2.9.3 访问权限的选择原则

2.10 Java 的继承方式

2.10.1 单继承

2.10.2 多层继承

2.10.3 多继承(不支持)

2.11 final 关键字:限制继承与修改

2.11.1 修饰类:禁止类被继承

2.11.2 修饰方法:禁止方法被重写

2.11.3 修饰变量:定义常量

2.11.4 final 的核心使用场景

2.12 继承与组合:代码复用的两种选择

2.12.1 继承与组合的对比

2.12.2 组合的实现示例

2.12.3 继承与组合的选择原则

三、多态(Polymorphism)

3.1 多态的核心概念

3.1.1 生活中的多态示例

3.1.2 Java 中的多态示例

3.2 多态的实现条件

3.2.1 条件详解

3.2.2 反例:缺少任一条件则无法实现多态

3.3 重写(Override):多态的核心实现手段

3.3.1 重写的规则

3.3.2 重写的示例

3.3.3 重写与重载的区别

示例:重写与重载的对比

3.4 动态绑定与静态绑定:多态的底层原理

3.4.1 静态绑定(前期绑定 / 早绑定)

3.4.2 动态绑定(后期绑定 / 晚绑定)

3.4.3 动态绑定的底层实现

3.5 向上转型与向下转型:多态中的类型转换

3.5.1 向上转型(Upcasting)

向上转型的本质

向上转型的使用场景

向上转型的优缺点

3.5.2 向下转型(Downcasting)

向下转型的本质

向下转型的安全实现:instanceof 关键字

向下转型的使用场景

3.6 多态的优缺点

3.6.1 多态的优点

3.6.2 多态的缺点

3.7 多态的常见陷阱与避坑指南

3.7.1 陷阱 1:在构造方法中调用重写的方法

3.7.2 陷阱 2:混淆静态方法与实例方法的多态性

3.7.3 陷阱 3:向下转型未做类型判断

3.7.4 陷阱 4:忽略访问权限对重写的影响

3.8 多态的实际应用场景

3.8.1 场景 1:框架中的统一接口设计

3.8.2 场景 2:业务逻辑中的策略模式

总结


前言

小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!

一、章节核心目标

  1. 掌握继承的概念、语法、核心机制及使用规范
  2. 理解组合的本质,能够在继承与组合之间做出合理选择
  3. 精通多态的实现条件、底层原理及实际应用场景
  4. 辨析重写与重载、向上转型与向下转型等易混淆概念
  5. 规避继承与多态使用过程中的常见陷阱

二、继承(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 继承的核心概念

继承是面向对象程序设计中实现代码复用的最重要手段,允许程序员在保持原有类特性的基础上扩展新功能,产生新的类(称为子类 / 派生类),被继承的原有类称为父类 / 基类 / 超类

继承的核心价值
  1. 代码复用:子类无需重复定义父类已有的成员(变量和方法),直接继承使用;
  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(); // 输出:元宝喵喵喵~~~
    }
}
继承的关键注意事项
  1. 子类会继承父类中所有非private修饰的成员变量和成员方法(private成员虽被继承,但无法直接访问);
  2. 子类必须体现特殊性:子类继承父类后,需新增特有成员(变量或方法),否则继承无意义(若子类与父类完全一致,直接使用父类即可);
  3. 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:子类与父类有同名成员方法

需区分两种核心场景:重载重写(重写将在多态章节详细讲解):

  1. 重载场景:父类与子类的同名方法参数列表不同(方法名相同,参数个数 / 类型 / 顺序不同),根据调用时传递的参数类型选择对应的方法:
// 父类
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)
    }
}
  1. 重写场景:父类与子类的同名方法参数列表、返回值类型完全一致(或返回值为父子关系),此时子类方法会覆盖父类方法,直接调用时优先执行子类方法;若需调用父类的重写方法,需使用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 的核心用法
  1. 访问父类的成员变量:当子类与父类有同名成员变量时,用super.变量名访问父类变量;
  2. 访问父类的成员方法:当子类重写父类方法时,用super.方法名(参数)调用父类的方法;
  3. 调用父类的构造方法:在子类构造方法中,用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 的注意事项
  1. 只能在非静态方法中使用:super依赖于子类对象的创建,而静态方法不依赖对象,因此在静态方法中使用super会编译报错;
  2. 构造方法中使用时需注意顺序:super(参数)必须是子类构造方法的第一条语句,且不能与this(参数)(调用子类自身构造方法)同时存在;
  3. 不能访问父类的private成员:super只能访问父类中protectedpublic或默认访问权限(同一包)的成员,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 子类构造方法的核心规则
  1. 子类构造方法中,必须调用父类的构造方法(显式或隐式);
  2. 若父类无无参构造方法,子类必须显式用super(参数)调用父类的有参构造方法;
  3. super(参数)必须是子类构造方法的第一条语句,且只能出现一次;
  4. 子类构造方法中,super(参数)this(参数)不能同时存在(两者都要求是第一条语句)。

2.7 super 与 this 的对比

superthis都是 Java 中的关键字,且都能用于访问成员变量、调用成员方法和构造方法,但核心定位和使用场景有明显区别:

对比维度thissuper
核心含义当前对象的引用子类对象中父类继承部分的引用
访问成员变量优先访问子类自身的成员变量,若无则向上追溯直接访问父类的成员变量
访问成员方法优先调用子类自身的方法,若无则向上追溯直接调用父类的方法
调用构造方法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 无继承关系的初始化顺序

无继承关系时,初始化顺序为:

  1. 静态代码块(只执行一次,类加载时执行);
  2. 实例代码块(每次创建对象时执行);
  3. 构造方法(每次创建对象时执行,在实例代码块之后)。

示例:

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 有继承关系的初始化顺序

存在继承关系时,初始化顺序为:

  1. 父类静态代码块(最早执行,只执行一次);
  2. 子类静态代码块(在父类静态代码块之后,只执行一次);
  3. 父类实例代码块(每次创建子类对象时执行);
  4. 父类构造方法(在父类实例代码块之后);
  5. 子类实例代码块(在父类构造方法之后);
  6. 子类构造方法(最后执行)。

核心原则:静态代码块优先于实例相关代码,父类相关代码优先于子类相关代码

示例:

// 父类
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 关键结论
  1. 静态代码块只在类加载时执行一次,无论创建多少对象,都不会重复执行;
  2. 实例代码块和构造方法每次创建对象时都会执行,且实例代码块始终在构造方法之前执行;
  3. 继承体系中,父类的初始化(静态 + 实例 + 构造)完全完成后,才会开始子类的初始化;
  4. 子类构造方法中的super(...)会触发父类的实例代码块和构造方法执行,但不会重复触发父类的静态代码块(类加载只执行一次)。

2.9 protected 关键字:继承中的访问控制

访问限定符用于控制类或类成员的访问范围,protected是专门为继承设计的访问限定符 —— 允许子类访问父类的成员,同时限制非子类的外部访问,平衡封装性和继承性。

2.9.1 访问限定符的范围对比

Java 中 4 种访问限定符的访问范围:

访问范围privatedefault(默认,无修饰符)protectedpublic
同一包中的同一类✔️✔️✔️✔️
同一包中的不同类✔️✔️✔️
不同包中的子类✔️✔️
不同包中的非子类✔️
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 访问权限的选择原则

实际开发中,选择访问权限的核心原则是 “最小权限原则”—— 尽可能使用更严格的访问权限,隐藏内部实现细节,只暴露必要的接口给外部,提高代码的安全性和可维护性。

具体选择建议:

  1. 成员变量:优先使用private,通过getter/setter方法暴露必要的访问;
  2. 成员方法:
    • 仅类内部使用:private
    • 同一包中的类使用:default
    • 子类需要继承使用:protected
    • 外部类(任何包)都需要使用:public
  3. 避免滥用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 的核心使用场景
  1. 保护核心类不被继承(如String、工具类);
  2. 保护核心方法不被重写(如父类的关键业务逻辑方法);
  3. 定义不可修改的常量(如配置参数、固定值);
  4. 局部变量修饰:避免方法内部变量被意外修改,提高代码可读性。

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 继承与组合的选择原则

实际开发中,选择继承还是组合,核心在于判断类之间的关系:

  1. 若类之间存在明确的 “is-a” 关系(如狗是动物、奔驰是汽车),且需要复用父类的大部分成员,同时子类需要重写父类的部分方法,则使用继承;
  2. 若类之间是 “has-a” 的组合关系(如汽车有发动机、人有手机),且希望降低代码耦合度、提高灵活性,则使用组合;
  3. 优先使用组合:组合的耦合度更低,扩展性更强,更符合 “合成复用原则”(面向对象设计原则之一),即 “尽量使用组合,而非继承来实现代码复用”。

例如,实现 “鸟会飞” 的功能:

  • 错误方式:定义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类型,但实际传入的是CatDog的对象。当调用animal.eat()时,程序会根据animal引用的实际对象类型,调用对应的eat()方法实现 —— 这就是多态的核心体现。

3.2 多态的实现条件

Java 中实现多态必须满足三个条件,缺一不可:

  1. 继承体系:子类必须继承自父类(或实现接口,接口多态本质是继承的延伸);
  2. 方法重写:子类必须重写父类中的方法(或接口中的抽象方法);
  3. 父类引用指向子类对象:通过父类类型的引用变量,指向子类类型的对象(即 “向上转型”)。
3.2.1 条件详解
  1. 继承体系:多态建立在继承基础上,没有继承就没有多态。子类通过继承获得父类的方法,为后续重写提供基础;
  2. 方法重写:重写是多态的核心 —— 如果子类不重写父类方法,那么父类引用调用的始终是父类的方法,无法体现 “不同对象不同表现”;
  3. 父类引用指向子类对象:这是多态的触发条件。只有当父类引用指向子类对象时,编译器无法确定调用的是父类还是子类的方法,需要在运行时根据实际对象类型动态绑定方法。
3.2.2 反例:缺少任一条件则无法实现多态
  1. 缺少方法重写:
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(); // 输出:吃食物(调用父类方法,无多态)
    }
}
  1. 缺少父类引用指向子类对象:
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 重写的规则

重写必须严格遵循以下规则,否则编译报错或无法构成有效重写:

  1. 方法原型一致:子类方法的方法名、参数列表(个数、类型、顺序)必须与父类方法完全一致;
  2. 返回值类型兼容:子类方法的返回值类型必须与父类方法的返回值类型相同,或为父类返回值类型的子类(即 “协变返回类型”);
  3. 访问权限不严格:子类方法的访问权限不能比父类方法的访问权限更严格(可以更宽松)。例如:
    • 父类方法为public,子类方法不能为protecteddefaultprivate
    • 父类方法为protected,子类方法可以为publicprotected(不能为defaultprivate);
  4. 不能重写的方法
    • static方法:静态方法属于类,不属于对象,无法重写(子类可定义同名静态方法,但这是隐藏父类方法,而非重写);
    • private方法:私有方法只能在父类内部访问,子类无法继承,因此不能重写;
    • final方法:最终方法禁止被重写;
    • 构造方法:构造方法是类特有的,不能被重写;
  5. 注解显式声明:子类重写方法时,建议添加@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 动态绑定(后期绑定 / 晚绑定)

动态绑定是指在运行时才确定方法调用与方法实现的关联,即方法调用的目标在编译阶段无法确定,需根据对象的实际类型动态选择。

典型场景:多态中的方法重写 —— 父类引用指向子类对象时,调用重写方法会触发动态绑定。

动态绑定的执行流程:

  1. 编译器检查父类中是否存在该方法(若不存在则编译报错);
  2. 运行时,JVM 获取当前对象的实际类型(子类类型);
  3. 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)机制:

  1. 每个类在加载时,JVM 会为其生成一个方法表,包含该类的所有方法(包括继承自父类的方法);
  2. 子类的方法表中,会覆盖父类方法表中同名、同参数列表的方法条目,指向子类的重写实现;
  3. 当通过父类引用调用方法时,JVM 会先获取对象的实际类型,然后查找该类型的方法表,找到对应的方法实现并调用。

方法表机制的优势是提高动态绑定的效率 —— 无需每次调用都遍历继承体系查找方法,只需直接查询方法表即可。

3.5 向上转型与向下转型:多态中的类型转换

在多态中,父类引用指向子类对象的过程称为 “向上转型”,而将父类引用还原为子类对象的过程称为 “向下转型”。类型转换是多态的重要组成部分,正确使用类型转换可以灵活操作子类对象。

3.5.1 向上转型(Upcasting)

向上转型是指将子类类型的对象赋值给父类类型的引用变量,语法格式为:

父类类型 引用变量名 = new 子类类型();

例如:

Animal animal = new Cat(); // 向上转型:Cat对象→Animal引用
向上转型的本质

向上转型是 “从小范围类型向大范围类型” 的转换(子类是父类的特殊类型,范围更小),例如 “猫是动物的一种”,因此转换是安全的,编译器不会报错。

向上转型的使用场景
  1. 直接赋值:将子类对象直接赋值给父类引用;
  2. 方法传参:父类类型作为方法参数,接收任意子类对象(多态的核心场景);
  3. 方法返回值:方法返回值为父类类型,可返回任意子类对象。

示例:向上转型的三种场景

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引用无法调用Catmew()方法或Dogbark()方法,因为编译器会将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

安全的向下转型流程:

  1. instanceof判断父类引用指向的对象是否为目标子类的实例;
  2. 若返回true,则进行向下转型;
  3. 若返回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 多态的优点
  1. 降低代码耦合度,减少冗余代码:通过父类引用统一操作子类对象,避免编写大量重复的分支判断(如if-elseswitch-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()方法即可,扩展性极强。

  1. 提高代码的可扩展性和可维护性:新增子类时,无需修改原有代码(如父类、测试类),只需保证子类符合父类的方法约定(重写父类方法),即可无缝集成到现有系统中,符合 “开闭原则”(对扩展开放,对修改关闭)。

  2. 提高代码的灵活性:父类引用可以指向任意子类对象,同一方法调用可根据对象类型动态切换行为,让代码更灵活、更通用。

3.6.2 多态的缺点
  1. 代码运行效率降低:多态依赖动态绑定,而动态绑定需要在运行时确定方法调用的目标,相比静态绑定(如重载、静态方法调用),会增加一定的运行时开销,降低代码执行效率(但这种开销在大多数场景下可忽略不计)。

  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); // 输出:动物(访问父类成员变量)
    }
}
  1. 构造方法没有多态性:构造方法是类特有的,不能被重写,因此无法通过多态调用子类的构造方法。

  2. 无法直接访问子类特有成员:向上转型后的父类引用无法直接调用子类特有的方法和成员变量,必须通过向下转型才能访问,增加了代码的复杂性。

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
    }
}

陷阱分析

  1. 创建D对象时,先调用父类B的构造方法;
  2. 父类B的构造方法调用func(),由于func()D重写,动态绑定会调用Dfunc()
  3. 此时D的构造方法尚未执行,成员变量num未初始化(默认值为 0),因此输出num=0,而非预期的1

避坑指南

  • 尽量避免在构造方法中调用实例方法(尤其是可能被重写的方法);
  • 若必须在构造方法中调用方法,优先调用privatefinal方法(这些方法无法被重写,不会触发动态绑定)。
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("吃鱼");
    }
}

避坑指南

  • 重写方法时,确保子类方法的访问权限≥父类方法的访问权限;
  • 建议父类方法使用publicprotected权限,子类重写时保持权限一致或更宽松。

3.8 多态的实际应用场景

多态在实际开发中应用广泛,以下是几个典型场景:

3.8.1 场景 1:框架中的统一接口设计

许多 Java 框架(如 Spring、MyBatis)都大量使用多态来设计统一接口。例如,Spring 的BeanFactory接口定义了获取 Bean 的统一方法,不同的实现类(如DefaultListableBeanFactoryAnnotationConfigApplicationContext)根据不同的场景提供具体实现,用户只需面向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学习笔记,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yvonne爱编码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值