玩转java多态

2021/04/08 Java高级知识 共 6942 字,约 20 分钟
闷骚的程序员

1. 多态概念回顾

Java中的多态,从字面理解即表示为多种形态。说白了就是相同的方法或者相同的调用方式,最终实际上调用的是不同形态的东西。
多态实现方式包括两种:

  1. 方法的多态:方法的重载和方法的覆盖。
  2. 对象的多态:对象的向上转型和向下转型。
    这两种方式的多态结合在一起使用,使用多态让java程序拥有了强大的百变能力。

2. 多态详解

2.1 没有了多态代码将变得多丑你知道吗?

  1. 如果没有多态,我认为接口就没有了存在的意义。因为没有多态的话,就没法向上转型,那要接口干啥?接口没法产生对象,就没法使用接口了。
  2. 如果没有多态,委托机制将几乎没有用武之地。A对象委托B,没有了多态,那么假如A对象要委托很多功能,将要定义很多类似B一样的新的类,并且A对象需要对每个委托对象进行委托,这样委托类A和被委托类都会很冗余。而我们期望的是,只委托一个B的接口或者父类型就能调用B的不同实现类的方法。这样A类很简洁,B类从设计上看也很优雅。
  3. 如果没有多态,几乎没法编写通用的处理方法。因为各种子类已经没法向上转型,没有了通用的类型去接收各种子类的实现,通用的方法怎么能通用呢?
  4. 如果没有多态,就没法完成方法覆盖和重载,那么每个方法只要逻辑不同,就完全是独立的,这样看起来满屏幕都是各种实现的方法,一点都不雅观。

2.2 多态的本质?

多态的本质就是调用相同名称的方法,结果能够实现不同的功能。
实际上这种能力是方法重载、方法覆盖和对象向上转型、向下转型共同作用的结果。

2.3 如何利用好多态?

使用多态,最常见的两种方式:

  1. 同一个类之间实现了方法重载。
  2. 父子类之间实现了方法的覆盖,然后疯狂的使用向上转型去调用。

3. 使用多态的几种案例

3.1 玩转方法重载

ClassA:

package zeh.test.demo.com.duotai;

public class ClassA {
    private String name;
    private String role;
    private String number;
    public ClassA() {
    }
    public ClassA(String name) {
        this.name = name;
    }
    public ClassA(String name, String role) {
        this(name);
        this.role = role;
    }
    public ClassA(String name, String role, String number) {
        this(name, role);
        this.number = number;
    }
    public void testClassA() {
        System.out.println("没有参数的test");
    }
    public void testClassA(String test1) {
        System.out.println(test1 + " sleep," + name + " is running");
    }
    public void testClassA(String test2, String test3) {
        System.out.println(test2 + " and " + test3 + " is fallback the " + this.role + " and " + this.number);
    }
}

MainClassA:

package zeh.test.demo.com.duotai;

// 2021-03-22
// zWX5331241
public class MainClassA {
    public static void main(String[] args) {
        // 使用多态的方式创建ClassA的实例
        ClassA classA = new ClassA();
        // 调用重载方法
        classA.testClassA();
        // 调用ClassA的一参构造
        classA = new ClassA("Eric");
        // 调用重载方法
        classA.testClassA("hello");
        classA.testClassA("Daisy", "Robbin");
        // 调用ClassA的三参构造
        classA = new ClassA("Zhaoeh", "student", "NO.1");
        classA.testClassA("JayChou", "JJ");
    }
}

3.2 通过父类或者接口玩转向上转型和重载

IService:

package zeh.test.demo.com.duotai;

// 定义顶层业务接口
// zWX5331241
// 2021-03-22
public interface IService {

    // 吃
    void eat();

    // 喝
    void drink();
}

AbstractBaseService:

package zeh.test.demo.com.duotai;

// 中间层的抽象类,用于定义业务执行模板。
// 模拟真实业务中,调用接口中的某个业务方法可能都需要一整套执行流程,因此此处设计一个中间层的抽象类,只用来固化接口方法中的每个方法需要的流程模板
public abstract class AbstractBaseService implements IService {

    // 覆盖eat方法,封装eat业务的一套流程
    @Override
    public void eat() {
        buyFood();
        cook();
        doEat();
    }
    // 要想eat,先得买食物,因为是模板,此处不实现,所以定义抽象方法。
    abstract void buyFood();
    // 买完食物需要烹饪,因为是模板,此处不实现,所以定义抽象方法。
    abstract void cook();
    // 开始主逻辑,即开吃,因为是模板,此处不实现,所以定义抽象方法。
    abstract void doEat();

    // 同理,覆盖drink方法,封装drink业务的一套流程
    @Override
    public void drink() {
        buyWater();
        doDrink();
    }
    // 要想喝水,先得买水,因为是模板,此处不实现,所以定义抽象方法。
    abstract void buyWater();
    // 开始喝水,因为是模板,此处不实现,所以定义抽象方法。
    abstract void doDrink();
}

BaseService:

package zeh.test.demo.com.duotai;

// 中间层的基类实现,该基类用于通用的业务流程,继承模板类,因此需要覆盖模板类中的所有抽象方法
// zWX5331241   2021-03-22
public class BaseService extends AbstractBaseService {
  
    // 覆盖模板中的 buyFood 逻辑。如果具体子类没有覆盖,那么子类将使用此处的覆盖逻辑。
    @Override
    void buyFood() {
        System.out.println("没人去的话,那就老子去买食物");
    }

    // 覆盖模板中的 cook 逻辑。如果具体子类没有覆盖,那么子类将使用此处的覆盖逻辑。
    @Override
    void cook() {
        System.out.println("没人去的话,那就老子去做饭");
    }

    // 覆盖模板中的 doEat 逻辑。如果具体子类没有覆盖,那么子类将使用此处的覆盖逻辑。
    @Override
    void doEat() {
        System.out.println("没人吃的话,那就老子自己吃");
    }

    // 覆盖模板中的 buyWater 逻辑。如果具体子类没有覆盖,那么子类将使用此处的覆盖逻辑。
    @Override
    void buyWater() {
        System.out.println("没人买水的话,那就老子去买");
    }
    
    // 覆盖模板中的 doDrink 逻辑。如果具体子类没有覆盖,那么子类将使用此处的覆盖逻辑。
    @Override
    void doDrink() {
        System.out.println("没人喝水的话,那就老子去喝");
    }
}

PersonService:

package zeh.test.demo.com.duotai;

// 人类的服务    zWX5331241  2021-03-22
public class PersonService extends BaseService {

    // 人类只覆盖了 doEat,那么 buyFood 和 cook 操作将默认使用继承链中距离当前子类最近的那个子类的实现,很明显,就是 BaseService 里面的实现。
    @Override
    void doEat() {
        System.out.println("老子你别吃了,你负责做就行了,我来吃哈哈哈哈");
    }
   
   // 人类只覆盖了 doDrink,那么 buyWater 操作将默认使用继承链中距离当前子类最近的那个子类的实现,很明显,就是 BaseService 里面的实现。
    @Override
    void doDrink() {
        System.out.println("老子你别喝了,你买回来我自己喝哈哈哈");
    }
}

CatService:

package zeh.test.demo.com.duotai;

// 猫的服务 zWX5331241  2021-03-22
public class CatService extends BaseService {
 
    // 覆盖 BaseService 中的 buyFood ,那么 buyFood 的行为将由Cat自己定义。
    @Override
    void buyFood() {
        System.out.println("我是猫猫,我需要的食物是猫粮,我得自己去买");
    }

    // 覆盖 BaseService 中的 cook ,那么 cook 的行为将由Cat自己定义。
    void cook() {
        System.out.println("我是猫猫,我需要的食物是猫粮,我得自己去加工");
    }

    // 覆盖 BaseService 中的 doEat ,那么 doEat 的行为将由Cat自己定义。
    @Override
    void doEat() {
        System.out.println("我是猫猫,我的猫粮只能我自己来吃喽");
    }
  
    // 覆盖 BaseService 中的 buyWater ,那么 buyWater 的行为将由Cat自己定义。
    @Override
    void buyWater() {
        System.out.println("我是猫猫,我需要的是特别干净的泉水,我得自己去买");
    }
    
    // 覆盖 BaseService 中的 doDrink ,那么 doDrink 的行为将由Cat自己定义。
    @Override
    void doDrink() {
        System.out.println("我是猫猫,我的水水其他人喝不了,我只能自己享用");
    }
}

TestMain:

package zeh.test.demo.com.duotai;

public class TestMain {
    public static void main(String[] args) {
        // 子类实例对象向上转型
        IService service = new PersonService();
        service.eat();
        service.drink();
        System.out.println("=========================" + service.getClass());
        // 子类实例对象向上转型
        service = new CatService();
        service.eat();
        service.drink();
        System.out.println("=========================" + service.getClass());
    }
}

执行结果:

没人去的话,那就老子去买食物
没人去的话,那就老子去做饭
老子你别吃了,你负责做就行了,我来吃哈哈哈哈
没人买水的话,那就老子去买
老子你别喝了,你买回来我自己喝哈哈哈
=========================class zeh.test.demo.com.duotai.PersonService
我是猫猫,我需要的食物是猫粮,我得自己去买
我是猫猫,我需要的食物是猫粮,我得自己去加工
我是猫猫,我的猫粮只能我自己来吃喽
我是猫猫,我需要的是特别干净的泉水,我得自己去买
我是猫猫,我的水水其他人喝不了,我只能自己享用
=========================class zeh.test.demo.com.duotai.CatService
Process finished with exit code 0

3.3 方法间参数传递对多态的助攻

上面的案例在使用多态时直接使用的是手工传递参数的方式,即直接使用 = 来进行多态的向上转型:

IService service = new PersonService();
service = new CatService();

然而在真实使用中,很多是在方法间进行参数传递时,灵活使用多态。
请看如下案例:
TestMain:

package zeh.test.demo.com.duotai;

public class TestMain {
    public static void main(String[] args) {
        // 子类实例对象向上转型
        IService service = new PersonService();
        serviceHandle(service);
        // 子类实例对象向上转型
        service = new CatService();
        serviceHandle(service);
    }
   
    // 现在服务允许Cat去操作,如果是Person的话输出拒绝操作的信息
    public static void serviceHandle(IService service) {
        if(service instanceof CatService){
            service.drink();
            service.eat();
        }
        if(service.getClass().equals(PersonService.class)){
            System.out.println("Person 拒绝操作");
        }
    }
}

测试结果:

Person 拒绝操作
我是猫猫,我需要的是特别干净的泉水,我得自己去买
我是猫猫,我的水水其他人喝不了,我只能自己享用
我是猫猫,我需要的食物是猫粮,我得自己去买
我是猫猫,我需要的食物是猫粮,我得自己去加工
我是猫猫,我的猫粮只能我自己来吃喽
Process finished with exit code 0

分析:
在多态中,一个对象通过向上转型后,传递给了父类对象,此时很多人不清楚现在这个对象到底是谁?
现在告诉你,任何一个对象通过向上转型后,这个对象本质上就是真正new出来的那个子类对象;即真正new的哪个类的对象,现在接收的就是哪个对象。
也就是说,上述案例中的“service.getClass()”实际上返回的是当前真正传递进来的子类对象实体所属于的类,而不可能是父类。
因此,在多态的传递中,有很重要的一点需要注意:
我们可以使用service.getClass()这种方式进行多态的逻辑分发,去判断当前流程传递进来的多态对象究竟是哪个Class。
当然,我们也可以使用instanceof去判断当前这个对象是否是某个具体类的对象。

tips:
为啥多态中使用了IService去接收了某个具体传入的对象,该对象调用getClass()方法返回的就是具体传入的对象呢?
答案:这实际上就是向上转型的原理,某个具体子类对象向上转型成为了IService对象,现在调用getClass()方法,如果子类覆盖了该方法就调用的是子类,否则调用的就是继承链中的某个最近父类的。
很显然,每个具体传入的对象自己都覆盖了getClass()方法,因此getClass()方法返回的就是真实传入的子类的class。

3.4 多态的逻辑分发

想一想我们平时都在用多态做啥?
似乎也想不起专门使用多态的地方。
然而当我们编写Controller,编写Service接口,编写Service实现类,当我们为Controller直接注入Service接口,为Service实现类直接注入Dao接口时,我们都在潜移默化的使用了多态。
在Java中,多态无处不在。
如果没有多态,那父子类之间将无法进行类型转换,那引用传递将处处受限;引用传递如果受限制,那java还玩个毛啊。
实际上我们已经知道,多态使用的方式无非就是方法重载、方法覆盖结合向上转型向下转型的共同作用。
但如何能用好多态,该是我们仔细思考的问题。
多态除了能在方法间使用父类对象或者接口对象去接收对应的子类实例进行间接的向上转型外,还有很重要的一个功能就是,多态能够实现逻辑分发。
正如上面的代码案例,如果说我们把 eat 和 drink 功能柔和在一起,那么对于不同的场景(比如Person和Cat),就需要我们不停在逻辑里面去判断这个请求是来自Person的请求还是来自Cat的请求。
这样将会导致正常的逻辑代码中出现一大堆 if else 之类的逻辑代码。
经过我们的分析,我们应该从业务模型中抽取出 Person 和 Cat。
抽取出来后,Person的逻辑就是用Person的那一套,Cat的逻辑就是用Cat的那一套。
对于Person和Cat共有的逻辑就通过父类来统一处理,对于各自需要的特殊逻辑就通过方法覆盖来进行处理。
如此一来,我们不用在代码中显式使用 if else 去判断请求到底来自哪个具体的实现类,而是直接通过多态,就能够将对应的流程转发到对应的实现子类中去。
这就叫做多态的逻辑转发。

文档信息

Search

    Table of Contents