本篇博客主要介绍Java中多态的实现方法。
多态
在学习多态之前,我们先来看几个和多态相关的概念。
我们写几个类来用来演示:
public class People {
public String name;
public People(String name) {
this.name = name;
}
public void buyTicket() {
System.out.println(this.name + "正在买票!");
}
}
public class Adult extends People{
public Adult(String name) {
super(name);
}
}
public class Student extends People{
public Student(String name) {
super(name);
}
}
向上转型
一句话来说明,向上转型就是使用一个父类的引用,指向一个子类的对象。如下:
People p1 = new Student("小明");
People p2 = new Adult("潘爱国");
为什么叫向上转型呢?
- 在面向对象的程序设计中,针对一些复杂的场景(很多类,很复杂的继承关系),程序猿会画一种叫做UML图的方式来表示类之间的关系。此时父类通常画在子类的上方。所以我们就称为“向上转型”,表示往父类的方向转。
向上转型发生的时机?
- 直接赋值。
- 方法传参。
- 方法返回。
有向上转型,自然就有向下转型:
- 向下转型就是将一个用父类引用指向的对象改为由子类引用来指向。
- 但是这个转换是有前提的,前提是父类引用指向的对象本身就是子类对象。否则就会转换出错。
所以我们需要在向下转型之前判断一下一个引用引用的是否是某个类型的对象,然后再进行向下转型。使用instanceof可以实现这个功能。
- 向下转型的使用场景:
People people = new Student();
这里使用一个父类引用指向一个子类对象,子类Student
中有一个方法上学goToSchool();
这时候我们使用父类引用来调用这个子类方法people.goToSchool();
。
可以看到这里找不到goToSchool();
方法,这是为什么呢?我们的people引用明明指向的是一个Student类对象。
这是因为,编译过程中people的类型是People,此时编译器只知道这个类中有一个buyTicket();
方法,没有goToSchool();
方法。虽然people实际引用的是一个Student类对象,但是编译器是以People的类型来查看有哪些方法的。
对于People people = new Student();
这样的代码。编译器检查有哪些方法存在时,看的是People这个类型。执行时究竟执行父类的方法还是子类的方法,看的是Student类型。
所以这个时候如果想要正常执行,达到我们想要的效果,就需要使用向下转型。
动态绑定
我们给Adult和Student子类都添加一个和父类同名的方法,来看一下。
下面,我们来看一段代码演示:
- 代码如下:
- 运行结果如下:
从运行结果,我们可以发现:
- p1和p2虽然都是People类型的引用,但是p1指向People类型的实例,p2指向Student类型的实例;
- p1和p2分别调用buyTicket方法时,发现
p1.buyTicket()
调用的是People类的方法,p2.buyTicket()
调用的是Student类的方法。
结论:
- 在Java中,调用某个类的方法,究竟执行了哪段代码(是父类方法的代码还是子类方法的代码),要看究竟这个引用指向的是父类对象还是子类对象。这个过程是程序运行时决定的(而不是编译期),因此称为动态绑定。
方法重写
对于上述,子类和父类中存在同名方法,并且参数的类型和个数完全相同,返回类型也必须相同,这种情况称为覆写/重写/覆盖(override)。
关于重写的注意事项:
- 重写和重载完全不一样。不要混淆。
重载要求:方法名称相同,参数的类型及个数不同,返回值类型没有影响;重载的函数在一个类中,没有权限要求。
重写要求:方法名称、返回值类型、参数的类型及个数必须完全相同;必须是继承关系中的两个方法;被覆写的方法不能拥有比父类更严格的访问控制权限。 - 普通方法可以重写,static修饰的静态方法不能重写。
- 重写中子类方法的访问权限不能低于父类方法的访问权限。日常使用中,我们经常将二者的访问权限设置成一样。
- 如果一个方法不能被继承,则不能重写它,例如:父类中private权限的方法。
- 针对重写方法,可以使用
@Override
注解来显式指定。
使用注解有两方面的功能:①能够让代码读者更好的理解这个方法是重写的;②能够在编译器做出一些检查,如果没有构成重写,能够检查出来。
理解多态
理解了向上转型、动态绑定和方法重写之后,我们就可以使用多态(polypepride)的形式来设计程序。
我们来写一个买票的例子,来理解多态:
- 代码如下:
首先,来写一个类表示人,其中有一个买票方法,什么也不做。
然后,继承该人类,写两个子类成人和学生,二者中分别对买票方法进行重写。
最后,我们写一个买票的函数,将两个对象传进去,感受一下多态的作用。前面为类的实现者,下面为类的使用者。
这里,我们可以看到,当类的调用者在编写buyTickets()
这个方法的时候,参数类型为People(父类),此时在该方法内部并不知道,也不关心当前的people引用指向的是哪个类型的实例。此时people这个引用调用buyTicket()
方法可能会有多种不同的表现(和people对应的实例有关),这种行为就称为多态。
多态顾名思义,就是“一个引用,能表现出多种不同形态”。
使用多态的好处
- 类调用者对类的使用成本进一步降低。
封装是让类的调用者不需要知道类的实现细节;
多态能让类的调用者连这个类的类型是什么都不必知道,只需要直到这个对象具有某个方法即可。
因此,多态也可以理解成是封装的更进一步,让类调用者对类的使用成本进一步降低。 - 能够降低代码的“圈复杂度”,避免使用大量if-else。
什么是圈复杂度?
圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙,那么就比较简单容易理解。而如果有很多的条件分支或者循环语句,就认为理解起来更复杂。
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称为“圈复杂度”。如果一个方法的圈复杂度太高,就需要重构。
不同公司对于代码的复杂度的标准不一样,一般不会超过10。
假如现在我们有5个人买票,不使用多态,代码如下:
使用多态,代码如下:
可以看到使用多态,少了if-else分支,代码的圈复杂度明显降低。
- 可扩展能力强。
还是前面的例子,如果我们需要在业务逻辑中加上另外一类人儿童,买票免费,我们来看一下使用多态和不使用多态分别需要怎么修改。
首先,我们先将儿童类创建出来。
看一下不使用多态,代码修改情况:
再来看一下使用多态的代码修改情况:
总结
多态其实是一个更广泛的概念,和“继承”这样的语法并没有必然的联系。
- C++中的“动态多态”和Java的多态类似。但是C++中还有一种静态多态(函数重载、泛型编程),就和继承没有关系了。
- Python中的多态体现的是“鸭子类型”,也和继承体系没有任何关系。
- Go语言中没有“继承”这样的概念,同样也能表示多态。
无论是哪种编程语言,多态的核心都是让调用者不必关注对象的具体类型。这是降低用户使用成本的一种重要方式。