概述
多态是继封装、继承之后,面向对象的第三大特性。
生活中,比如求面积的功能,圆、矩形、三角形实现起来是不一样的。跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来也是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。那么此时就会出现各种子类的类型。
Java是强类型静态语言,既每一个变量在使用之前必须声明它确切的类型,然后之后的赋值和运算时都是严格按照这个数据类型来处理的。
但是,有的时候,我们在设计一个数组、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。
- 例如:想要设计一个数组用来存储各种图形的对象,并且按照各种图形的面积进行排序,但是具体存储的对象可能有圆、矩形、三角形等,那么各种图形的求面积方式又是不同的。
- 例如:想要设计一个方法,它的功能是比较两个图形的面积大小,返回面积较大的那个图形对象。那么此时形参和返回值类型是图形类型,但是不知道它具体是哪一种图形类型。
这个时候,Java就引入了多态。
实现多态的前提条件
- 有继承
- 有方法的重写
- 父类的引用指向子类的对象
多头的格式体现:
在多态的情况下,代码有2种状态。以等号为界,左边是编译时状态,右边是运行时状态
- 编译时,看“父类”,只能调用父类声明的方法或者父类继承下来的可见方法,不能调用子类扩展的方法;
- 运行时,看“子类”,如果调用的方法被子类重写,一定是执行子类重写的方法体。否则调用本类或者其父类的方法
代码示例
class Animal {
public void eat() {
System.out.println("动物吃饭...");
}
public void show() {
System.out.println("我是父类独有的方法");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("我是狗。我吃骨头");
}
//子类特有的方法
public void lookHome() {
System.out.println("我是狗,我看家");
}
}
public class Test {
public static void main(String[] args) {
/*
编译时状态 运行时状态
Animal ani = new Dog();
*/
//父类的引用指向子类的对象
Animal animal = new Dog();
/*
编译时,看“父类”,只能调用父类声明的方法或者父类继承下来的可见方法,不能调用子类扩展的方法;
*/
//animal.lookHome();错误:不能调用子类扩展的方法
/*
运行时,看“子类”,如果调用的方法被子类重写,一定是执行子类重写的方法体。否则调用本类或者其父类的方法
*/
//调用的方法被子类重写
animal.eat(); //我是狗。我吃骨头
//调用父类自己声明的方法,没有被子类重写
animal.show(); //我是父类独有的方法
//调用父类继承下来的方法,没有被子类重写
System.out.println("animal.toString() = " + animal.toString()); //animal.toString() = sgg1.demo02.Dog@4554617c
}
}
多态的应用
多态应用在形参实参:父类类型作为方法形式参数,实参可以是自己或者其所有的子类对象。
class Animal {
public void eat() {
System.out.println("我是动物,我吃饭..");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("我是狗,我吃骨头");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("我是猫,我吃鱼");
}
public void catchMouse() {
System.out.println("我是猫,我抓老鼠");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
showEat(dog); //实参给形参赋值 Animal a = new Dog() 多态引用
showEat(cat); //实参给形参赋值 Animal a = new Cat() 多态引用
}
/*
* 设计一个方法,可以查看所有动物的吃的行为
* 关注的是所有动物的共同特征:eat()
* 所以形参,设计为父类的类型
* 此时不关注子类特有的方法
*/
public static void showEat(Animal animal) {
animal.eat();
// animal.catchMouse(); 错误,因为animal现在编译时类型是Animal,只能看到父类中有的方法
}
}
/*
运行结果:
我是狗,我吃骨头
我是猫,我吃鱼
*/
多态应用在数组:数组元素类型声明为父类类型,实际可以存储自己或者其所有的子类对象
/*
* 声明一个数组,可以装各种动物的对象,
*/
//在堆中开辟了长度为2的数组空间,用来装Animal或它子类对象的地址
Animal[] arr = new Animal[2];
arr[0] = new Cat();//多态引用 左边arr[0] 是Animal类型,右边是new Cat()
arr[1] = new Dog();//把Dog对象,赋值给Animal类型的变量
多态应用在返回值:方法的返回值类型声明为父类的类型,实际返回值可以是自己或者是其所有的子类对象
/*
* 设计一个方法,可以购买各种动物的对象,此时不确定是那种具体的动物
*
* 返回值类型是父类的对象
*
* 多态体现在 返回值类型 Animal ,实际返回的对象是子类的new Cat(),或new Dog()
*/
public static Animal buy(String name){
if("猫咪".equals(name)){
return new Cat();
}else if("小狗".equals(name)){
return new Dog();
}
return null;
}
向上转型与向下转型
一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。这个和基本数据类型的转换是不同的。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
多态的弊端
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。
解决方式:
- 想要调用子类特有的方法,必须做类型转换。
向上转型
当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 此时,一定是安全的,而且也是自动完成的
// 向上转型
Animal a = new Cat();
向下转型
当左边的变量的类型(子类)<右边对象/变量的类型(父类),我们就称为向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 此时,不一定是安全的,需要使用(类型)进行强制类型转换
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException
// 向上转型
Animal a = new Cat();
// 向下转型
Cat c = (Cat)a;
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse
}
}
}
那么,哪些instanceof判断会返回true呢?
- 对象/变量的编译时类型 与 instanceof后面数据类型是直系亲属关系才可以比较
- 对象/变量的运行时类型<= instanceof后面数据类型,才为true
代码示例
class Animal {//父类
void eat() {
System.out.println("~~~");
}
}
class Cat extends Animal {//子类
public void eat() {
System.out.println("吃鱼");
}
public void catchMouse() {//子类独有方法
System.out.println("抓老鼠");
}
}
class Dog extends Animal {//子类
public void eat() {
System.out.println("吃骨头");
}
public void watchHouse() {//子类独有方法
System.out.println("看家");
}
}
public class Test {
public static void main(String[] args) {
showInfo(new Dog()); // 向上转型 Animal a = new Dog();
showInfo(new Cat()); // 向上转型 Animal a = new Cat();
}
private static void showInfo(Animal animal) {
animal.eat();
// instanceof 关键字,给引用变量做类型的校验
if (animal instanceof Dog) {
// 向下转型
Dog dog = (Dog) animal;
dog.watchHouse(); // 调用的是 Dog 的 watchHouse
} else if (animal instanceof Cat) {
// 向下转型
Cat cat = (Cat) animal;
cat.catchMouse(); // 调用的是 Cat 的 catchMouse
}
}
}
多态下成员变量引用的原则
如果直接访问成员变量,那么只看编译时类型
class Father{
int a = 1;
}
class Son extends Father{
int a = 2;
}
/*
* 成员变量没有重写,只看编译时类型
*/
public class TestExtends {
public static void main(String[] args) {
Son s = new Son();
System.out.println(s.a);//2,因为son的编译时类型是Son
System.out.println(((Father)s).a);//1 ((Father)son)编译时类型,就是Father
Father s2 = new Son();
System.out.println(s2.a);//1 son2的编译时类型是Father
System.out.println(((Son)s2).a);//2 ((Son)son2)编译时类型,就是Son
}
}
多态下成员方法引用的原则
非虚方法:只看编译时类型
在Java中的非虚方法有三种:
- 由invokestatic指令调用的static方法,这种方法在编译时确定在运行时不会改变。
- 由invokespecial指令调用的方法,这些方法包括私有方法,实例构造方法和父类方法,这些方法也是在编译时已经确定,在运行时不会再改变的方法
- 由final关键字修饰的方法。虽然final方法是由invokevirtual指令进行调用的,但是final修饰的方法不能够进行在子类中进行覆盖,所以final修饰的方法是不能够在运行期进行动态改变的。在java语言规范中明确规定final方法就是非虚方法。
虚方法:静态分派与动态绑定
在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
当我们通过“对象.方法”的形式,调用一个虚方法,我们要如何确定它具体执行哪个方法呢?
- 静态分派:先看这个对象的编译时类型,在这个对象的编译时类型中找到最匹配的方法。最匹配的是指,实参的编译时类型与形参的类型最匹配
- 动态绑定:再看这个对象的运行时类型,如果这个对象的运行时类重写了刚刚找到的那个最匹配的方法,那么执行重写的,否则仍然执行刚才编译时类型中的那个方法