上篇我们总结了封装和继承两大块内容,这篇我们一起来看一下Java中的多态。
多态,意指方法或者对象具有多种形态,个人认为本质上还是为了减少代码的冗余度,增加代码的复用性。多态是建立在封装和继承的基础之上的。
先来说一下方法多态,我们前面提到的方法重载、方法重写就体现了多态。方法重载的多态表现在,同一个方法名,根据传入的参数不同,可以有不同的用法;而方法重写的多态体现在,不同对象调用同一个方法,结果可能是不同的。即它们都能体现出方法的多种形态。
再来分析一下对象多态,对象多态的前提是,两个对象存在着继承关系。个人理解对象的多态体现在:一父类引用可以指向许多不同的子类对象,等于说一个对象引用可以引用到多种形态的对象,而不只是单一的类型。
那么它是怎么做到的呢?我们先来讲一下编译类型和运行类型。如果要死记硬背的话可以记成“编译在左,运行在右”。即一个对象的编译类型和运行类型可以不一致。
这里我们先简单介绍一下编译和运行。
编译就是将代码交给编译器进行语法检查,比如是否缺少分号,赋值语句中值是否越界等等,如果没有错误就生成.class字节码文件,且编译的时候不做任何的内存分配操作;而运行是将编译好的字节码文件交给Java虚拟机执行,如果没有出现逻辑错误,比如空指针异常,除0语句,越界访问,就能正常运行,且只有在运行的时候才会确定内存真正的分配大小以及地址。
比如我们
// Animal是父类,Dog和Cat是它的子类
Animal animal = new Dog();
animal = new Cat();
先看第一条语句,此时animal引用的编译类型就是Animal,而运行类型是Dog。编译类型是在我们定义animal变量的时候就确定了,它是一个animal类型的引用变量,是不会变的,而运行类型是可以改变的,比如我们接下来可以令animal指向Cat类,运行到第二条语句的时候,animal的运行类型就变成了Cat。
这就是Java中的向上转型,即一个父类引用可以引用到父类底下的所有子类对象。
那么问题来了,当我们使用父类引用去调用属性和方法的时候,调用的究竟是父类的还是子类的?
父类引用调用属性:先说结论,调用属性看这个引用的编译类型,编译类型是哪个类,就调用哪个类里面的对应属性。
我们如果用父类引用去调用子类的特有属性的时候,会发现在编译的时候就会报错,因为编译器它不管你这个运行类型是什么,它只纠正语法错误,那么一个类只能是调用其自己的属性或者是父类的属性,而不能调用到其子类的特有属性。
如果父类和子类中有属性重名了,这时,编译是可以通过了,但是运行的时候我们还是会调用到编译类型所对应的属性。前面我们也提到过,父类和子类的属性在子类对象中是有独立的内存空间的,互不干扰。当我们在运行时,虚拟机只会根据引用的类型,去找到堆中对应的属性空间里面的属性。也就是在向上转型中,父类引用只能调用自己的属性或者是父类的父类的属性。
我们再来看看父类引用调用方法:
还是先说结论:调用方法看这个引用变量的运行类型,运行类型是哪个类,就调用哪个类的对应方法。
之所以会发生这种情况,是因为Java中调用某个对象的方法的时候,调用的这个方法会和对象的内存地址绑定,也就是所谓的动态绑定机制,按照先寻找绑定的对象,找不到再找其父类的顺序进行查找。可以认为,绑定的这个内存地址是Java开始查找调用的方法的地方。而我们上面有提到,内存地址是只有在运行的时候才会分配的,也就是会根据运行类型来分配内存地址,所以这个时候,调用的是运行类型所对应的方法。
还是用一个例子说明:
public class PolyTest {
public static void main(String[] args) {
Person person = new Student();//向上转型
System.out.println(person.info());
}
}
class Person{
private int age = 18;
public int info(){
return getAge();
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Student extends Person{
private int age = 7;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
这个时候会打印出7还是18?
答案是7。
我们一起来分析一下,当person调用info方法的时候,究竟发生了什么?
首先,因为person 的运行类型是Student,所以Java先去Student类里面找,发现没有找到,此时继承机制就发挥作用,此时去找它的父类Person,找到了info方法,发现方法体里面调用了getAge方法,此时根据动态绑定机制,Java还是先去Student类里面找,找到了getAge,所以此时返回的是Student里定义的age,7。
我们在上一篇提到的方法的重写,其实本质也是这样的,子类引用变量引用了子类对象,查找调用的方法的时候肯定是从运行类型->子类对象,来查找的。只不过此时这个引用变量的编译类型和运行类型是一样的。而此时我们先找到子类中重写的方法,找到后就会返回,不会再往上找父类的方法了,这就是方法重写的本质。
这里接着再说一下我们上一篇中挖的坑:子类在进行方法重写的时候,方法名和参数必须一样,而返回类型和修饰符可以不一样,但要满足一定规则。
1、子类重写方法的修饰符不能缩小父类方法所设定修饰符的范围。
2、子类重写方法的返回类型必须是和父类方法的返回类型相同或者是其返回类型的子类。
为什么要这样规定呢?
我的个人理解是,前面说到,多态是建立在封装和继承的基础之上的,而我们前面提到的向上转型,要求父类引用可以引用到子类对象,这也隐式地规定了父类引用在调用父类方法的时候是不能出现编译错误的。如果方法被子类重写了,根据我们的动态绑定机制,我们是会调用到子类里重写的方法,此时要让父类引用正常调用,那么访问修饰符的范围就只能扩大、相等而不能缩小。同理,要正常接收该重写方法的返回值,也隐式地要求返回的值类型必须是相等,或者是其子类(这里又是一个向上转型),这样才能被正常接收。
按照这样去理解的话,这两条规则也就不难记了。
差不多要理完了,还差一个向下转型。
有向上转型,自然就有向下转型。还是用代码来说明:
Animal animal = new Dog();//向上转型
Dog dog = (Dog)animal;//正确的向下转型
Cat cat = (Cat)animal;//错误的向下转型
第一行是我们前面提到的向上转型,此时父类引用指向子类对象。
第二行是正确的向下转型,即此时我们把指向Dog对象的父类引用强转成Dog的引用。这里要注意的是,只有引用能被强转,而对象是不能被强转的噢~
向下转型我们可以写成:子类类型 引用名 = (强转类型)父类引用
这里来分析一下这个式子的隐含条件,我们先看右边:要强转一个父类引用,那么这个强转类型必须是父类引用指向的类或者是这个类的父类。因为如果强转类型是父类引用指向对象的子类,那么相当于把一个子类引用指向其父类对象,这是不允许的。
分析完强转类型,我们再来看一下左边,子类类型和强转类型的关系又是什么呢?我们首先要明确,父类引用被强转之后,已经是强转类型的引用了。此时的限制条件在于,强转类型的引用能够赋值给子类类型的引用,则子类类型必须是和强转类型相同的类型或者是其父类。这是因为一个子类引用可以赋给父类引用,反之不行,在编译上就过不去。即使我们知道父类能够向上转型指向其子类,可能这个子类引用和父类指向的类就是同一个类,可是还是不能直接这样赋值。
其实我们仔细想一下,为什么会有向下转型,不就是因为父类的引用不能直接赋值给子类引用,所以才需要先强转再赋值吗?
分析到这里其实差不多了,本质就是父类可以指向子类,反之不行。这里还要说一下,Java中继承关系只是在父类和子类之间,拥有相同父类的两个子类之间是没有直接关系的。比如我们的第三行代码,是一个错误的向下转型。可以理解为,子类和父类之间有继承关系,父类引用可以指向子类对象,子类引用只能指向子类的子类对象或者是和自己相同类型的对象,而不能是其他没有直接关系的类的对象。
这里的Cat类和Dog类,它们有着相同的父类,但它们本身却没有什么直接关系。可以用语文的角度去理解,猫和狗都是动物,但猫和狗没有直接关系,也可以用我们提到的数据结构->树来理解,兄弟节点除了拥有同一个父节点之外,没有其他的直接关系,所以子类引用不能指向其对应的兄弟节点的对象。
最后,总结一下这篇文章的内容:
Java中的多态可以分为方法的多态和对象的多态,其本质就是一个东西(方法名、对象引用)可以对应不同的方法或者对象类型,也就是有不同的形态。方法的多态可以在方法重载和方法重写上面体现,而对象的多态其难点就在于向上转型,本质上是父类引用指向子类对象,这里就会出现父类引用调用属性和方法的问题。调用属性是遵循编译类型是哪个就调用哪个类型的属性的原则,调用方法则是要遵循动态绑定机制,这里我理解的是运行类型绑定了方法查找的起点,也就是从运行类型开始往上找调用的方法,找到就返回。向下转型就是为了让引用子类对象的父类引用将此指向赋给子类引用,这个比较好理解,但是要注意分析强转类型的隐含限制,以及强转类型和子类类型的隐含关系。子类对象不能指向其“兄弟节点”所对应的类,其他毫无关系的类的对象就更不行了。
在应用中,向上转型和向下转型会经常配合使用,比如我们用父类作为形参,用子类作为实参,这就是一个向上转型。而当我们需要调用子类中的特有方法的时候,就需要将父类引用强转成子类引用,然后调用子类的方法和属性,这就是向下转型。
以上就是我对封装、继承、多态的个人理解和总结,欢迎大家批评指正~