目录

一.引言

        (一).什么是多态?

        (二).多态在面向对象三大特征中的重要性

        (三).为什么多态重要?解决了什么问题?

二.多态的两种类型

        (一).编译时多态

        (二).运行时多态

        (三).二者区别对比

三.运行时多态的实现条件

四.核心机制讲解

        (一).动态绑定

        (二).JVM在运行时决定调用哪个方法

        (三).instanceof关键字的使用

        (四).向上转型vs向下转型,ClassCastException风险

五.接口与抽象类中的实现

        (一).通过接口实现多态

        (二).抽象类VS接口在多态场景下的选择

        (三).代码对比

六.多态的优缺点

七.常见误区与陷阱

八.总结

九.面试常见考点

十.实战演练


一.引言

        (一).什么是多态?

          简单来说,同一个方法,不同的对象调用所产生的结果也不一样,就叫多态。

           我们来看这样一段代码:

class Animal{
    String name;
    int age;

    public Animal(String name,int age){
        this.name = name;
        this.age = age;
    }
    
    public void move(){
    System.out.println(this.name + "正在跑");
    }
}

//Fish类继承Animal类,并且重写move()方法
class Fish extends Animal{

     public Fish(String name,int age){
        super(name,age);
     }
    
    public void move(){
    System.out.println(this.name + "正在游泳");
    }
}

//Dog类也继承Animal类,并且重写move()方法
class Dog  extends Animal {
    public Dog(String name,int age){
        super(name,age);
     }
    
    public void move(){
    System.out.println(this.name + "正在跑步");
    }
}



public class Test{
    public static void main(String[] args){
        Animal fish = new Fish("彩鳞",1);//这里发生了向上转型
        Animal dog = new Dog("小白",4);
        fish.move();
        dog.move();
    }
}

               运行结果如下:

         

                 

                同样调用move()方法,但是对象不同,一个是fish,一个是dog,结果也不同,这个就                 叫多态

                我们注意到,上面的代码发生了这几件事情:

                1.Dog类和Fish类都继承了Animal类

                2.Dog类和Fish类重写了Animal类的mova()方法
                3.发生了向上转型。(下面会讲解,这里先知道有这个就可以)

                这就是触发多态的条件:

                1.有继承关系

                2.子类重写父类的方法

                3.发生了向上绑定

        (二).多态在面向对象三大特征中的重要性

                面向对象三大特征的关系为:

                封装(基础) → 继承(前提) → 多态(核心体现),多态建立在封装和继承之上,是最能                 体现面向对象的设计价值的特征。

                如果没有多态,我们就只能这样设计:

        

class Animal{
    String name;
    int age;

    public Animal(String name,int age){
        this.name = name;
        this.age = age;
    }
}

//Fish类继承Animal类
class Fish extends Animal{

     public Fish(String name,int age){
        super(name,age);
     }   
}

//Dog类也继承Animal类
class Dog  extends Animal {
    public Dog(String name,int age){
        super(name,age);
     }
}



public class Test{
    public static void main(String[] args){
        Fish fish = new Fish("彩鳞",1);/
        Dog dog = new Dog("小白",4);
        if (animal instanceof Fish) {
        System.out.println(animal.name + "正在游泳");
    } else if (animal instanceof Dog) {
        System.out.println(animal.name + "正在跑步");
    }
    }
}

                每次新增一个类,都要判断新的对象隶属于哪个类,大大降低了代码的灵活度。

                总结来说,封装实现了对内部实现的隐藏保护数据安全。继承实现了代码的复                 用,建立类之间的层级关系,多态让程序灵活应对变化,对外保持统一接口。

                

        (三).为什么多态重要?解决了什么问题?

                多态的核心价值:

                1.降低耦合,对外统一接口。

                        无论什么动物,只需要animal.move();

                2.对扩展开放,对修改关闭。

                        比如代码新增一个bird类,我们只需要在bird类中也重写move方法,完全不用                         改动其他的move方法

                3.是所有设计模式的基础

                        几乎所有设计模式的核心都依赖多态来实现灵活替换。

                补充对比:多态的不足之处:

                1.多态的代码效率比较低

                2.成员变量和构造方法不能使用多态

二.多态的两种类型

        (一).编译时多态

                编译时多态的代表是方法的重载。编译器在编译是就能确定调用哪个方法。

                重载是在一个类中,方法名相同,参数不同(参数个数不同或参数的顺序不同或参数的类型不同),通过传入不同的参数来调用对应方法。

                这里声明一下,学术界对于重载算不算多态存在争议。因为多态是不同对象调用同一个方法所产生的不同的行为。而重载是同一个对象的不同方法,所以我们认为重载是“最弱 的”多态

                我们可以理解为,重载是同一个接口(方法名),不同的表现形式。

        (二).运行时多态

                重写是运行时多态的经典代表。JVM 在运行阶段才能确定调用哪个方法。

Animal fish = new Fish("彩鳞", 1);
Animal dog  = new Dog("小白", 4);

fish.move(); // 运行时才知道:fish 实际是 Fish,调用 Fish 的 move()
dog.move();  // 运行时才知道:dog  实际是 Dog, 调用 Dog  的 move()

         编译器看到的是 Animal 类型,它不知道具体是哪个子类,只有程序真正跑起来之后,JVM 才查看对象的真实类型,再决定调用谁的方法。

        (三).二者区别对比

编译时多态和运行时多态对比
编译时多态 运行时多态
实现方式 方法重载 方法重写

决定时机

编译阶段 运行阶段
判断依据 根据参数列表 对象的实际类型
是否需要继承 不需要 需要
灵活性
典型场景 同一个类中的多种操作 向上转型

三.核心机制讲解

        (一).动态绑定

        

1.动态绑定的定义:

        在继承的大条件下,当运行代码的时候,调用了父类和子类中重写的那个方法,结果实际调用了子类的方法,我们把这个情况叫做叫做动态绑定。

2.动态绑定的触发条件:

        (1).父类的引用 指向了 子类的对象

        (2).父类和子类存在重写的方法

3.动态绑定注意事项:

        如果父类的构造方法中,调用了父类和子类发生重写的方法,那么会发生动态绑定

        (二).instanceof关键字的使用

     我们可以通过 inStanceOf 关键字来判断对象是否属于类,是则返回true,否则返回flase;

 

//定义Animal类,设置name和age属性,给构造方法和move()方法
class Animal {
    String name;
    int age;
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void move() {
        System.out.println(this.name + " 正在移动");
    }
}

//Fish类继承Animal类,在构造方法中调用父类的构造方法,重写move()方法,给呼吸的方法
class Fish extends Animal {
    public Fish(String name, int age) {
        super(name, age);
    }
    @Override
    public void move() {
        System.out.println(this.name + " 正在游泳");
    }
    public void breatheInWater() {
        System.out.println(this.name + " 在水中呼吸");
    }
}

//Dog类继承Animal类,在构造方法中调用父类的构造方法,重写move()方法,给叫的方法
class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }
    @Override
    public void move() {
        System.out.println(this.name + " 正在跑步");
    }
    public void bark() {
        System.out.println(this.name + " 汪汪叫");
    }
}

//定义Fish和Dog类的对象,判断对象是否属于某个类
public class Test {
    public static void main(String[] args) {

        Animal fish = new Fish("彩鳞", 1);
        Animal dog  = new Dog("小白", 4);

        // 判断对象是否属于某个类
        System.out.println("=== 基本判断 ===");
        System.out.println(fish instanceof Fish);    // true
        System.out.println(fish instanceof Animal);  // true,Fish 继承了 Animal
        System.out.println(fish instanceof Dog);     // false

        //  实际用途:向下转型前的安全检查
        // 父类引用无法直接调用子类独有的方法
        // fish.breatheInWater();  编译报错,Animal 没有这个方法

        System.out.println("\n=== 安全向下转型 ===");
        if (fish instanceof Fish) {
            Fish f = (Fish) fish;       // 确认是 Fish 之后再强转,安全
            f.breatheInWater();         //  现在可以调用 Fish 独有的方法
        }

        if (dog instanceof Dog) {
            Dog d = (Dog) dog;
            d.bark();                   //  调用 Dog 独有的方法
        }

        //  不用 instanceof 直接强转会怎样?
        System.out.println("\n=== 不检查直接强转的后果 ===");
        try {
            Dog d = (Dog) fish;         //  fish 实际是 Fish,强转成 Dog
            d.bark();
        } catch (ClassCastException e) {
            System.out.println("报错了:" + e.getMessage()); // 运行时抛出异常
        }
    }
}

 运行结果如下:

        (三).向上转型vs向下转型,ClassCastException风险

        

向上转型:

1.定义:

        向上引用指得是子类的对象 赋值给 父类的引用,例如:

Animal animal = new Dog();

2.特点:

        (1).安全,无风险

        (2).引用是父类,对象是子类

        (3).可以使用父类的属性和方法(如果子类重写了,优先执行子类的重写方法 ),但是不能使用子类独有的属性。

3.三种实现方法:

(1).直接赋值:

Animal animal = new Dog();

(2).方法传参:

public void test(Animal animal){
}

(3).返回值:

public Animal test(){
  Dog dog = new Dog();
  return dog;
}

publlic static void main(){
  Animal animal = test();
}

向下转型:

1.定义:

        向下转型是重新定义一个引用,创建一个新的子类与父类引用指向同一个对象。通过强制类型转换把父类的引用指向强制类型转换后的引用,代码如下:

Animal animal = new Dog();//向上引用
Dog dog = (Dog)animal;//向下引用
dog.worf;//强转后的引用可以调用子类本身的方法,也可以调用父类继承下来的方法

2.注意事项:

        (1).不是所有的向下转换都能成功(强制类型转换存在风险)

        (2).向下转换可以让引用使用子类对象的成员方法

四.接口与抽象类中的实现

        (一).通过接口实现多态

// 定义接口
interface Animal {
    void move();      // 接口中的方法默认是 public abstract
    void eat();
}

// Fish 实现 Animal 接口
class Fish implements Animal {
    String name;
    public Fish(String name) {
        this.name = name;
    }
    @Override
    public void move() {
        System.out.println(this.name + " 正在游泳");
    }
    @Override
    public void eat() {
        System.out.println(this.name + " 在吃水草");
    }
}

// Dog 实现 Animal 接口
class Dog implements Animal {
    String name;
    public Dog(String name) {
        this.name = name;
    }
    @Override
    public void move() {
        System.out.println(this.name + " 正在跑步");
    }
    @Override
    public void eat() {
        System.out.println(this.name + " 在啃骨头");
    }
}

// Bird 实现 Animal 接口
class Bird implements Animal {
    String name;
    public Bird(String name) {
        this.name = name;
    }
    @Override
    public void move() {
        System.out.println(this.name + " 正在飞翔");
    }
    @Override
    public void eat() {
        System.out.println(this.name + " 在吃虫子");
    }
}

public class Test {
    // 参数类型是接口,任何实现了 Animal 接口的对象都可以传进来
    public static void doAction(Animal animal) {
        animal.move();
        animal.eat();
    }

    public static void main(String[] args) {
        // 接口引用指向实现类对象,和父类引用指向子类对象是同样的道理
        Animal fish = new Fish("彩鳞");
        Animal dog  = new Dog("小白");
        Animal bird = new Bird("小黄");

        System.out.println("=== 直接调用 ===");
        fish.move();
        dog.move();
        bird.move();

        System.out.println("\n=== 通过方法统一调用 ===");
        doAction(fish);
        System.out.println("---");
        doAction(dog);
        System.out.println("---");
        doAction(bird);
    }
}

        (二).抽象类VS接口

                

1.抽象类的概念和实现:

        抽象类的概念:抽象类是被abstract修饰的类,抽象类不能被实例化,只能作为其他类的父类而使用。

2.抽象方法:抽象方法是被abstract修饰的方法,抽象方法可以没有具体的实现

        (1).抽象类和抽象方法的实现:

public abstract class Animal{
    public String name;
    public int age;
    
    public abstract eat(){
      System.out.println(this.name + "正在吃……");
    }
}

(2).抽象方法和抽象类的关系:

        一个抽象类中可以没有抽象方法,但是抽象方法必须在抽象类中。抽象类中可以包含其他普通的成员方法和成员变量

        

3.抽象类和抽象方法的注意事项:

(1).我们不能实例化抽象类,因为抽象类是不完整的

(2).抽象类的抽象方法在继承后必须重写,否则违反了非抽象类中不能有抽象方法的准则

(3).抽象类就是为了被继承而生

(4).抽象方法必须满足重写所需要的五个条件:不能被final,static,private修饰,可以构成赋值关系,权限限定符

// 定义抽象类
abstract class Shape {
    String color;

    public Shape(String color) {
        this.color = color;
    }

    // 抽象方法:没有方法体,子类必须重写
    public abstract double getArea();
    public abstract double getPerimeter();

    // 普通方法:子类直接继承,不需要重写
    public void printInfo() {
        System.out.println("图形:" + this.getClass().getSimpleName()
                         + ",颜色:" + this.color
                         + ",面积:" + this.getArea()
                         + ",周长:" + this.getPerimeter());
    }
}

// 圆形
class Circle extends Shape {
    double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}

// 矩形
class Rectangle extends Shape {
    double width;
    double height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }

    @Override
    public double getPerimeter() {
        return 2 * (width + height);
    }
}

// 三角形
class Triangle extends Shape {
    double a, b, c; // 三条边

    public Triangle(String color, double a, double b, double c) {
        super(color);
        this.a = a;
        this.b = b;
        this.c = c;
    }

    @Override
    public double getArea() {
        // 海伦公式
        double s = (a + b + c) / 2;
        return Math.sqrt(s * (s - a) * (s - b) * (s - c));
    }

    @Override
    public double getPerimeter() {
        return a + b + c;
    }
}

public class Test {
    public static void main(String[] args) {

        //  抽象类不能实例化
        // Shape shape = new Shape("红色"); // 编译直接报错

        //  抽象类引用指向子类对象
        Shape circle    = new Circle("红色", 5);
        Shape rectangle = new Rectangle("蓝色", 4, 6);
        Shape triangle  = new Triangle("绿色", 3, 4, 5);

        System.out.println("=== 各自调用抽象方法 ===");
        // 同一个方法名,不同子类,结果不同 —— 多态
        System.out.printf("圆形面积:%.2f%n",       circle.getArea());
        System.out.printf("矩形面积:%.2f%n",       rectangle.getArea());
        System.out.printf("三角形面积:%.2f%n",     triangle.getArea());

        System.out.println("\n=== 调用继承来的普通方法 ===");
        // printInfo() 是抽象类里的普通方法,三个子类都没有写这个方法
        // 但是都能用,因为继承了抽象类
        circle.printInfo();
        rectangle.printInfo();
        triangle.printInfo();
    }
}

八.面试常见考点

        1. 什么是多态?多态有哪几种类型?

        2. 多态的实现条件是什么?

        3. 重载和重写(的区别?

        4. 什么是动态绑定?与静态绑定有什么区别?

        5. instanceof 关键字的作用与多态中的使用场景

        6.static修饰的方法能重写嘛?为什么?

        7.. 多态的好处与弊端?

        8.抽象类和接口在多态中的角色?

九.实战演练

        

灵活的支付系统

背景

你正在开发一个电商系统的支付模块。系统需要支持多种支付方式:支付宝支付微信支付银行卡支付。未来还可能增加新的支付方式。每种支付方式的处理流程略有不同,但都包含两个基本动作:扣款和 退款

需求

  1. 定义一个支付接口 Payment,包含两个方法:

    • boolean pay(double amount):支付指定金额,返回是否成功。

    • void refund(double amount):退款指定金额(直接输出退款信息即可)。

  2. 实现三种具体的支付类,分别命名为 AliPayWeChatPayCardPay,都实现 Payment 接口。

    • 每个类的构造方法可以接收必要的身份标识。

    • pay 方法中:输出“通过 [支付方式] 支付了 xx 元”,并返回 true(。

    • refund 方法中:输出“通过 [支付方式] 退款了 xx 元”。

  3. 多态场景

    • 创建一个 PaymentProcessor 类,其中包含一个 void processPayment(Payment payment, double amount) 方法,接收 Payment 接口引用,完成支付操作(调用 pay)。

    • 额外提供一个 void cancelOrder(Payment payment, double amount) 方法,接收 Payment 引用,完成退款操作(调用 refund)。

  4. 特殊需求

    • 对于 CardPay 类,额外增加一个方法 void deductPoints(int points),表示使用银行卡积分抵扣。

    • 在 processPayment 方法中,如果传入的是 CardPay 类型,除了支付外,自动调用 deductPoints(100)(模拟本次支付赠送积分,或使用积分)。

    • 要求使用 instanceof 检查并向下转型。

  5. 测试场景:编写主类 PaymentTest

    • 创建支付宝、微信、银行卡三种支付对象。

    • 使用 PaymentProcessor 分别处理它们的支付(多态方式调用)。

    • 单独测试退款方法(多态方式)。

    • 特意传入银行卡支付对象,观察积分抵扣逻辑是否正确执行。

  6. 扩展要求(可选):

    • 新增一种支付方式 CryptoPayment(加密货币支付)。pay 方法中输出“通过加密货币支付了 xx 元”,refund 输出对应退款。

    • 在不修改 PaymentProcessor 和主类核心逻辑的前提下,验证新的支付方式能否被无缝集成。 

框架如下:

// Payment.java
public interface Payment {
    boolean pay(double amount);
    void refund(double amount);
}

// AliPay.java
public class AliPay implements Payment {
    private String account;
    public AliPay(String account) { this.account = account; }
    // TODO 实现pay和refund方法
}

// WeChatPay.java
public class WeChatPay implements Payment {
    private String openId;
    public WeChatPay(String openId) { this.openId = openId; }
    // TODO 实现pay和refund方法
}

// CardPay.java
public class CardPay implements Payment {
    private String cardNumber;
    public CardPay(String cardNumber) { this.cardNumber = cardNumber; }
    // TODO 实现pay和refund方法
    // 额外方法
    public void deductPoints(int points) {
        System.out.println("银行卡积分抵扣 " + points + " 积分");
    }
}

// PaymentProcessor.java
public class PaymentProcessor {
    public void processPayment(Payment payment, double amount) {
        // TODO: 调用pay方法,并利用instanceof处理CardPay特有的积分抵扣
    }
    public void cancelOrder(Payment payment, double amount) {
        // TODO: 调用refund方法
    }
}

// PaymentTest.java
public class PaymentTest {
    public static void main(String[] args) {
        PaymentProcessor processor = new PaymentProcessor();
        
        Payment alipay = new AliPay("alice@example.com");
        Payment wechat = new WeChatPay("wx_123456");
        Payment card = new CardPay("6222****1234");
        
        System.out.println("=== 支付场景 ===");
        processor.processPayment(alipay, 100.0);
        processor.processPayment(wechat, 50.0);
        processor.processPayment(card, 200.0);
        
        System.out.println("=== 退款场景 ===");
        processor.cancelOrder(alipay, 20.0);
        processor.cancelOrder(card, 50.0);
        
        
         Payment crypto = new CryptoPayment("0x...");
         processor.processPayment(crypto, 300.0);
    }
}

十.小结        

        这个实战题目涵盖了多态的核心应用场景:接口统一、动态绑定、类型判断与扩展性。动手敲一遍代码,你会更清楚地理解“父类引用指向子类对象”到底带来了怎样的灵活性。现在就去运行它,然后试着增加一种新的支付方式吧——你会看到,多态让代码几乎不需要改动就能自然扩展。

更多推荐