一、基本概念

什么是多态?简单概述,多态就是【一个接口,多种实现】,关注接口本身,不关注具体的实现或者行为是什么。

举个生活中的例子,你有小王和小张两个好基友,经常一起嗨皮。和小王一起时你们经常喜欢去图书馆,和小张时你们喜欢去体育场。某一天小王和小张分别问:今天怎么安排?你答:老样子。那么,他们理解的老样子大概率不会是去同一个地方。

在这个过程中,询问“怎么安排”这个事件本身就是一个接口,你(接口的调用者)所做的动作都是一样的(告知“老样子”安排),但是理解这个回答的人(接口的实现者)不同,要去的地方(具体实现行为)也不同。所以可以看出,多态其实就是调用者和实现者之间定义了一套公认的标准(方法),由调用者去调用实现者的方法,对于调用者而言只关注实现者是谁,不关注实现者怎么实现。达到【同一套指令,不同执行】的效果。

对于具体语言来说,java这种编译型语言要求被调用者实例必须可以向上转型为调用者类型,需要被调用者【显式继承父类】或者【实现接口】。而python这种解释型语言就没有这种强制性要求。根本原因在于对于编译型语言函数参数是有类型的,所以传入的参数对象都要可以被认为是同一种类型。而python的函数参数并没有类型限制,所以不需要这些继承的约束,两个完全不相关的类可以同时作为接口的实现类(python中,多态的实现方式主要就是通过鸭子类型机制。鸭子类型的含义是:当看到一只鸟走起来像鸭子、游泳像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子)。

二、python中的多态实现

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
# @author: atwdy@foxmail.com
# @time: 2025/11/20 11:28
# @desc: python多态(鸭子类型机制)
"""


class Singer:
    def intro(self):
        print("I'm a singer")


class Coder:
    def intro(self):
        print("I'm a coder")


class CommonUtils:
    def intro(self, obj):
        obj.intro()


if __name__ == '__main__':
    utils = CommonUtils()
    utils.intro(Singer())  # I'm a singer
    utils.intro(Coder())  # I'm a coder

三、java中的多态实现

1.接口实现

/**
 * @author atwdy@foxmail.com
 * @since 2025-11-18 19:11
 * 接口方式
 */

interface Person {
    void intro();
}

class Singer implements Person {
    @Override
    public void intro() {
        System.out.println("I'm a singer");
    }
}

class Coder implements Person {
    @Override
    public void intro() {
        System.out.println("I'm a coder");
    }
}

class CommonUtils {
    public void intro(Person person) {
        person.intro();
    }
}


public class DuotaiDemo {
    public static void main(String[] args) {
        CommonUtils uitls = new CommonUtils();
        uitls.intro(new Singer());  // I'm a singer
        uitls.intro(new Coder());   // I'm a coder
    }
}

2.继承实现

/**
 * @author atwdy@foxmail.com
 * @since 2025-11-18 19:11
 * 继承方式
 */

abstract class Person {
    abstract void intro();
}

class Singer extends Person {
    @Override
    public void intro() {
        System.out.println("I'm a singer");
    }
}

class Coder extends Person {
    @Override
    public void intro() {
        System.out.println("I'm a coder");
    }
}

class CommonUtils {
    public void intro(Person person) {
        person.intro();
    }
}


public class DuotaiDemo {
    public static void main(String[] args) {
        CommonUtils uitls = new CommonUtils();
        uitls.intro(new Singer());  // I'm a singer
        uitls.intro(new Coder());   // I'm a coder
    }
}

3.对java中转型的正确总体认知

在理解下面的向上转型和向下转型之前,我觉得对java中的转型有一个正确的认知比较重要,也更容易理解当前场景下为什么要这样转,代码实现中为什么有这样那样的限制,不同转型后方法的查找调用顺序是什么。

首先需要明确,java中正确理解转型需从两个层面来认识,1)代码编译期;2)代码执行时。

在java中,转型并不会改变实例对象的真实类型,只是改变了编译器层面对这个实例对象的类型认知

在编译期时,编译器会把这个对象视为转型后的类型,所以检查的是转型后的类型中是否符合一些基本要求,比如是否包含实例调用的方法,这可以理解为是编译器对调用方法的一个安全检查,保证被调用的方法在调用者类型的视野范围内(显式存在或者父类继承)。

而在执行时看的是实例对象的真实类型(new出来时的类型),因为转型不改变实例对象的真实类型,所以在方法调用时无论调用者是原始类型还是转型之后的类型,都不会影响实例方法的调用顺序,都可以理解为先从实例被new出来的那个真实类型中开始查找调用,没有的话沿继承链从下往上查找直到找到第一个为止。

基于此,可以更容易理解下面向上转型和向下转型两种场景的一些特点。

1)向上转型(编译看左,运行看右)

这句话在java中很经典了,几年没写java了还能记得这句话。意思是多态无论是通过接口还是继承方式实现、父类无论是普通类还是抽象类、接口或父类中的方法无论是抽象的还是普通的,接口或父类中都必须得存在这样的一个同名intro方法。因为CommonUtils类中的intro方法,是通过调用Person类型实例的intro方法实现的。上面在调用CommonUtils类中的intro方法传参时,本质上是进行Person person = new Singer()这么的一个操作。

所以在编译期时,编译器会检查Person类型中有没有intro这个方法,没有就会编译报错(编译看左)。而在代码实际执行时,则会去调用真正传递进去的实例类型的intro方法,如果有就调用,没有的话沿着继承链向上查找(运行看右)。

2)向下转型(编译看右,运行看右)

这里说的向下转型,是指一个实例对象在经过向上转型后,再被转回到原始类型或者原始类型和向上转型类型之间的中间类型,而不是说直接将一个实例对象向下转型,java中不允许将一个未向上转型的对象直接向下转型。

这种类型转换限制个人觉得可以从数据转换中精度损失的视角来理解。一般来说小类型转大类型都是无精度损失的,比如float转double,但是反之就不行,会损失精度。其实java中的对象也可以这么理解,接口或者父类相对子类而言更像是一种允许能力复刻和延伸的模板,子类默认继承所有父类具有的属性和能力,但在此基础之上又可以对继承过来的属性或能力进行个性化DIY或者增加自己独有的属性或能力,从这个角度上来看父类更像是一种通用类型,子类更像是一种具体类型,因此将小类型向大类型转换是合理的,就像42码的脚穿上了44码的鞋,包容的下。但反过来就不行,因为子类很有“特长”,确实塞不进去。

首先需要明确,一般什么样的场景才需要向下转型。根据上面【编译看左,运行看右】的规则,当用大类型的对象去调用一个大类型中没有而小类型中独有的方法时,在编译期就会出现在大类型中找不到这个方法报错的情况,因此一般在需要调用小类型中的某个特有方法时才会向下转型

而对于【编译看右,运行看右】的理解,“运行看右”说的还是方法的查找调用顺序取决于实际对象类型,和上面向上转型中没有区别。“编译看右”指的是,调用的方法要出现在向下所转类型的视野中,否则编译器报错不通过。

/**
 * @author atwdy@foxmail.com
 * @since 2025-11-18 19:11
 * 向下转型测试
 */

class Person {
    public void intro() {
        System.out.println("I'm a person");
    }
}

class Singer extends Person {
    @Override
    public void intro() {
        System.out.println("I'm a singer");
    }
}

class FishLeong extends Singer {
    @Override
    public void intro() {
        System.out.println("I'm Fish Leong");
    }

    public void sing() {
        System.out.println("宁静的夏天...");
    }
}


public class DuotaiDemo {
    public static void main(String[] args) {
        Person person = new FishLeong();
        ((FishLeong) person).intro();   // 合法 输出“I'm Fish Leong”
        ((Singer) person).intro();      // 合法 输出“I'm Fish Leong”
        ((FishLeong) person).sing();    // 合法 输出“宁静的夏天...”
//        ((Singer) person).sing();       // 不合法 Singer类型中没有sing方法
    }
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐