多态概述
多态性是继封装性和继承性之后,面向对象的第三大特性。
多态性是面向对象编程的又一个重要特征,它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
对面向对象来说,多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是大家通常所说的多态性。
多态的体现
在Java中对象的多态性体现在父类引用指向子类的对象。
首先定义3个类,Person、Man、Woman,其中Person是Man和Woman的父类。
Person类:
public class Person {
String name;
int age;
public void eat(){
System.out.println("吃饭");
}
public void walk(){
System.out.println("走路");
}
}
Man类:
public class Man extends Person {
boolean isDrinking;
boolean isSmoking;
public void earnMoney(){
System.out.println("男人挣钱的方法");
}
@Override
public void eat() {
System.out.println("为了有力气干活,我要吃2斤大肘子");
}
@Override
public void walk() {
System.out.print("男人迈着霸气的步伐");
}
}
Woman类:
public class Woman extends Person{
boolean isBeauty;
public void goShopping() {
System.out.println("女人购物的方法");
}
@Override
public void eat() {
System.out.println("为了大漂亮,我只能吃二两水煮大白菜");
}
@Override
public void walk() {
System.out.print("女人迈着婀娜的步伐");
}
}
多态之前实例化对象
引用变量是什么类型就new什么类型的对象
public void test1() {
// 引用变量类型为Person,new的也是Person
Person p1 = new Person();
Man man = new Man();
Woman man = new Woman();
}
使用多态实例化对象
父类引用指向子类对象(或子类对象赋值给父类引用)
// 格式:
父类类型 变量 = new 子类对象;
public void test2() {
// 引用为父类,new的对象为子类
Person p1 = new Man();
Person p2 = new Woman();
}
Java的引用变量有两个类型,等号左边类型称为编译时类型,等号右边类型称为运行时类型。
编译时类型:声明引用变量的类型。
运行时类型:实际赋给引用变量的类型。
当编译时类型和运行时类型不一致时,就产生了对象的多态性。
向上转型
如果是第一次接触多态,可以会非常迷惑。为什么会存在这种定义的方式?
多态的定义方式,其实就是一种向上转型的过程。对于如下的代码大家肯定很容易理解:
short a = 10;
int b = a; // 自动类型提升
我们知道基本数据类型间可以发生自动类型转换。比如将一个short类型的变量赋值给int类型,范围小的类型会自动提升为范围大的类型。如果对应到类的继承关系中,显然父类的适用范围更加广泛,而子类的适用范围更加具体。因此我们可以将子类的向上转型看作是基本类型的自动类型转换。
至于为什么叫做向上转型而不叫向下转型,在继承关系图中一般是将父类作为上一个层级的存在,而子类作为下一个层级的存在,当子类需要类型转换为父类时,一般都形象的称为向上转型。而向下转型则表示子类的引用指向父类的对象,这个概念将在本文后续介绍。
大家肯定会有疑问,为什么子类的适用范围要小于父类?明明子类继承了父类所有的属性和方法,并且还可以拥有自己特有的方法,这不是意味着子类用于的功能比父类更多嘛?
让我们回到对于类的概念上,类是作为对一类具有相关属性和行为的事物的描述。其中的属性和行为越多对于事物的描述也就越具体。所以对比子类和父类,显然对于子类的描述更加的具体,而对于父类的描述更加的抽象,父类所涵盖的范围也就更加广泛。
向上转型的三种时机
- 直接赋值
public void test2() {
// 向上转型,子类对象赋值给父类引用
Person p1 = new Man();
Person p2 = new Woman();
Person p3;
Man m1 = new Man();
p3 = m1; // 父类引用指向了子类引用所指向的子类对象(表达有点绕,其实是一样的)
}
- 方法的传参
public void polymorphic(Person p1){
//...
}
public void test2() {
// 传参中将子类对象作为实参传递给方法的父类引用形参。
polymorphic(new Man());
}
- 方法的返回值
// 方法的返回值为Person,将Person的子类对象作为返回值传递
public Person polymorphic1(){
Man m = new Man();
return m;
}
public void test3(){
// 接收方法的返回值也应该使用Person类型的引用变量接收
Person p1 = polymorphic1();
}
子类向上转型后发生的改变
当子类向上转型为父类后,其引用类型就是父类类型,通过父类的引用是无法访问到子类对象中特有的属性和方法,只能访问父类中存在的属性和方法。
// 试图通过父类的引用访问子类特有的属性和方法时,编译会报错
public void test2() {
Person p1 = new Man();
Person p2 = new Woman();
p1.isDrinking = false;
p1.isSmoking = false;
p1.earnMoney();
p2.isBeauty = false;
p2.goShopping();
}
多态的使用
调用方法
如果大家可以接受向上转型的机制,我们接下来继续看看当通过多态进行调用方法时,会发生什么情况。
public void test3(){
Person p1 = new Man();
Person p2 = new Woman();
// 通过多态的方式调用父类存在的方法
p1.eat();
p1.walk();
p2.eat();
p2.walk();
}
从输出结果这里看到,使用多态的方式调用父类中存在的方法时,实际上调用的是子类覆盖重写后的方法。这里需要引入方法绑定的概念。
绑定是指将一个方法调用同一个方法主题关联起来,Java中的绑定分为静态绑定(前期绑定)和动态绑定(后期绑定)。
静态绑定,程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。在Java语言中,private、static、final所修饰的方法以及构造器在编译期间已经被绑定,编译器准确的知道应该调用哪个方法,这种调用方式被称作静态绑定。
动态绑定,程序执行前编译器不知道对象的实际类型,在程序运行期间,JVM通过父类引用获取其引用对象的实际类型,并定位到方法区中的该类的方法表,找到对应的方法进行调用。在Java中除了上述提到的几种静态绑定方法以外,其它所有的方法都属于动态绑定。而动态绑定是实现多态的关键。
Java在调用方法时,编译期间会首先确定父类引用中是否存在所需要对应的方法,如果没有则会向其父类中查找,直到找到Object类,如果Object类中仍然没有该方法则会编译报错。
如果存在该方法时,编译期间所调用的就是父类中存在的方法。在多态的前提下(即子类重写了父类的方法时)父类被调用的方法也被称作虚拟方法。
在运行期间,就发生了上面所介绍的动态绑定,JVM会根据所调用的方法前的引用,找到实际的运行时类型并调用其中的所对应方法,如果没有则向上查找。在本例中如果Man和Woman类中没有覆盖重写父类的方法,那么实际调用的还是父类的方法。不过这种情况完全没有必要再使用多态的方式创建对象。
总结多态在调用方法时的特点就是:编译看左,运行看右。
调用属性
多态性并不适用于属性,因为在继承性中属性是不会被重写的,这也意味着子类中的重名属性是属于子类特有的属性与父类无关。以多态的方式调用属性时遵循编译看左,运行也看左的规则。
- 编译看左:在编译期间会确定父类中是否存在对应属性,没有则编译错误。
- 运行看左:运行期间父类引用所调用属性也是父类中的属性,即使子类中存在重名属性也不会调用该属性。
例如在子类中定义一个与父类同名的属性:
当使用多态的方式调用name
属性时,实际上调用的时父类的name
。
多态存在的必要条件
-
继承
多态必须是在继承的基础之上才能产生的,如果两个类不存在继承关系,则更谈不上下面所需的重写和向上转型。
-
重写
如果子类没有覆盖重写父类的方法,那么多态的所要体现出的动态调用不同方法的意义也就不存在。因此也就没有必要使用多态创建对象。
-
向上转型
向上转型也就是父类引用指向子类的对象,通过这种方式,可以使得一个父类(接口)接受不同的子类对象完成各自不同的功能。
多态的优点
使用多态可以提高我们开发的便利性和代码的拓展性,如何理解这段话呢?
例如,需要定义一个功能,该功能实现了对餐厅对人类对象的服务,餐厅并不关心其所服务的对象是男人还是女人,只要是人该餐厅都能为其提供服务。
为了完成上述的功能,如果在没有多态的前提下,我们需要分别为男人和女人同时定义一个功能相同的方法。
// 为男人定义该功能
public void Service(Man m){
m.walk();
System.out.println("走进餐厅");
System.out.println("请问您需要些什么?");
m.eat();
System.out.println("好的请您稍等");
}
// 为男人定义该功能
public void Service(Woman f){
f.walk();
System.out.println("走进餐厅");
System.out.println("请问您需要些什么?");
f.eat();
System.out.println("好的请您稍等");
}
如果使用多态来完成该项功能,我们可以只需要将接收对象的类定义为两个类的父类就可以在一个方法中完成对不同对象的服务。
public void Service(Person p){
p.walk();
System.out.println("走进餐厅");
System.out.println("请问您需要些什么?");
p.eat();
System.out.println("好的请您稍等");
}
这里例子只存在两个子类,如果是需要服务非常多的类,并且这些类都具有公共父类时,多态的好处就体现的非常明显,可以使得我们极大的减少冗余的代码。
同时在扩展性方法,不管今后添加多少子类,我们都可以使用这同一个方法来完成。
向下转型
在前文中已经介绍了向上转型的概念,而向下转型的概念就是子类的引用指向父类的对象。
首先需要明确的是向下转型属于强制类型转换,我们类比到基本数据类型的强制类型转换,基本数据类型在强制类型转换时可能会发生数据截断,而作为对于类来说这个过程同样是不安全的。因此在实际开发中需要谨慎使用。
向下转型的格式
向下转型同样使用强制类型转换符()
来进行转型,定义格式如下:
子类类型 变量名 = (子类类型) 父类变量名;
例如:使用Man的引用接收Person的对象,当程序运行时会发生什么?
Man m = (Man) new Person();
程序抛出了类型转换异常ClassCastException
。我们分别从概念和代码层面上来讨论为什么程序会报错。
从概念上来说
一个男人可以是一个人(is a
),但我们不能说一个人是一个男人,显然人的表示范围更加广泛。因此从道德伦理上来说,这种行为也是反常态的。
从代码上来说
子类中的属性和方法一定是等于或者多于父类中的属性和方法,那么我们如果我们将一个父类的对象成功赋值给子类的引用时,通过这个子类的引用去调用子类中特有的方法,父类的对象是不存在这些方法的,这显然就会发生更加严重的错误。所以这种行为是不被允许的。
那如果我们将一个向上转型的Woman类强转为Man类时又会发生什么呢?
Person p = (Man) new Woman();
Man m = (Man) new Person();
程序也抛出了类型转换异常。同样从概念和代码上进行讨论
从概念上来说
让一个女人变成一个男人显然不是Java力所能及的范围。
从代码上来说
两个继承于同一个父类的不同子类他们的内部结构肯定是不相同的,如果允许这种行为的发生,同样会导致无法预测的错误。
instanceof
上面的举例说明了向下转型中存在的隐患,那么向下转型就没有用了嘛?显然也不能一概否定,当我们在对子类对象进行向上转型后又需要向下转型来调用子类特有的属性和方法时,向下转型的作用就体现出来了。但是鉴于上面可能发生的异常,我们应该在向下转型之前使用instanceof
操作符来判断该对象是否为需要转型后的对象。
使用格式
引用变量 instanceof 类A // 检查该引用所指向的对象是否为类A的实例,如果是返回true,否则返回false
例如:定义一个方法,该方法使用了父类的引用接收对象,在其中需要调用子类所特有的属性和方法。
public void instanceOfTest(Person p) {
if (p instanceof Man) {
Man m = (Man) p;
m.earnMoney();
m.isDrinking = false;
} else if (p instanceof Woman) {
Woman f = (Woman) p;
f.goShopping();
f.isBeauty = true;
}
}
对于向下转型和instanceof
操作我们应该尽量减少使用,因为这种转型是无法被编译器察觉出错误的,只有在程序运行期间才会抛出异常。当我们需要使用子类特有的方法时,应该首先检查父类的设计是否合理,而不是直接使用向下转型。
总结
关于多态性,属于面向对象三大特性中最为抽象的一种特性,对于它的理解也更加困难,具体还需要落实到代码层面多多体会,理解好了多态性对于抽象类和接口的理解也会更加深刻。
最后引用《Java编程思想》
作者 Bruce Eckel 的一句话:不要犯傻,如果它不是晚绑定(动态绑定), 它就不是多态。