多态
对于某一个对象(事物),在不同的时刻体现出来的不同状态叫做多态
如: 水的液态、固态和气态
学生在上课时间上课,在吃饭时间吃饭,在休息的时候睡觉
在java中,对象变量是多态的,一个超类对象除了可以引用一个自身类对象,还可以引用它的派生类对象。通过继承和方法重写来实现多态。
对象间的转型问题
在java中,每个对象变量都属于一个类型,类型描述了这个变量所引用的以及能够引用的对象类型, 将一个值存入变量时,编译器会检查是否允许该操作
1.向上转型:将一个子类的引用赋给一个超类变量,编译器是允许的,不用进行强制类型转换。
格式: 超类 超类变量=new 子类();
2. 向下转型:但是将一个超类的引用赋给 一个子类变量,必须进行强制类型转换,这样才能够通过运行时的检查
格式: 子类 子类对象变量名=(子类)父类对象引用
class Animal
{
public int age=40;
public void eat()
{
System.out.println("吃东西");
}
public void sleep()
{
System.out.println("睡觉");
}
}
class Cat extends Animal
{
public int age=5;
//重写了父类的方法
public void eat()
{
System.out.println("猫吃老鼠");
}
public void playGame()
{
System.out.println("玩毛线球");
}
}
class Dog extends Animal
{
public int age=3;
//重写了父类的方法
public void eat()
{
System.out.println("狗啃骨头");
}
}
class AnimalTest
{
public static void main(String[] args)
{
//向上转型:不需要强制类型转换
Animal a=new Cat();
System.out.println(a.age); //调用的是父类的成员变量
a.eat(); //调用的是子类Cat的方法
//a.playGame(); //多态的弊端:无法访问子类特有的方法 error:类型为Animal的变量a找不到方法playGame()
a.sleep(); //调用的是子类Cat的方法
/*
结论:当超类类变量引用子类对象
访问成员变量时:访问的是父类的成员变量
访问成员方法时:先访问的是超类的此方法,如果此方法被子类覆盖,则调用子类的方法。并且不能访问子类特有的方法
*/
System.out.println("------------");
//向下转型:需要强制类型转换
Cat cat=(Cat)a;
System.out.println(cat.age); //访问的是子类Cat的成员变量
cat.eat(); //访问的是子类Cat的方法
cat.playGame(); //可以访问子类Cat特有的方法
//Cat cat2=a; //error:Animal无法转换为Cat
System.out.println("------------");
//Dog dog=(Dog)a;
/*上条语句在编译时并不会出现错误,这是因为编译器认为超类变量a可能是Dog类,但是在运行时会抛出一个ClassCastException(类型转换异常)
如果没有捕获这个异常,那么程序就会异常终止,如何解决这个问题呢?
在进行类型转换之前,先查看是否能够成功的转换,通过使用instanceof操作符来实现。如下
*/
if(a instanceof Dog)
{
Dog dog=(Dog)a;
System.out.println("可以进行转换");
}
else
{
System.out.println("不可以进行转换");
}
System.out.println("-----------instanceof 操作符-----------------");
System.out.println(a instanceof Dog);
System.out.println(a instanceof Cat);
System.out.println(null instanceof Cat);
System.out.println(a instanceof Animal);
}
}
instanceof (二元)操作符:
boolean result=Object instanceof class
参数:
- Object: 必选。任意对象表达式
- class: 必选。任意已定义的对象类
含义:如果Object是class的一个实例,则返回true,如果Object不是class的一个实例或者为null时,返回false
运行结果
多态的好处:
1. 应用程序不必为每一个派生类编写功能调用,只需要对抽象超类进行处理即可。提高了程序的可复用性。(通过继承来实现)
2. 派生类的功能可以被超类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。
多态的弊端:不能使用子类特有的方法
下面的例子可以看出使用多态时,在使用不同的Animal子类时,都利用了同一个Tool.eat()方法,提高了程序的复用性。同时,在创建新的子类时,也可以继续使用Tool.eat()方法而不用进行修改,因此使用多态可以提高代码的可扩充性和可维护性。
但是当使用父类对象变量引用子类对象时,派生类Cat特有的playGame()方法不能被调用。
class Animal
{
public int age=40;
public void eat()
{
System.out.println("吃东西");
}
public void sleep()
{
System.out.println("在晚上睡觉");
}
}
class Cat extends Animal
{
public int age=5;
//重写了父类的方法
public void eat()
{
System.out.println("猫吃老鼠");
}
public void playGame()
{
System.out.println("猫喜欢玩毛线球");
}
}
class Dog extends Animal
{
public int age=3;
//重写了父类的方法
public void eat()
{
System.out.println("狗啃骨头");
}
}
class Pig extends Animal
{
public void eat()
{
System.out.println("猪吃白菜");
}
}
class Tool
{
public static void eat(Animal animal)
{
animal.eat();
animal.sleep();
}
}
class AnimalTest2
{
public static void main(String[] args)
{
Cat cat=new Cat();
Tool.eat(cat);
Dog dog=new Dog();
Tool.eat(dog);
Pig pig=new Pig();
Tool.eat(pig);
System.out.println("-------------------");
Animal a=new Cat();
//a.playGame(); //error:类型为Animal 的变量a 找不到方法 playGame();
Tool.eat(a);
}
}
运行结果
多态的其他实现方式:
方法调用
如何调用方法呢?假设要调用x.f(args),Son是Father的子类
1.编译器查看对象的声明类型(即隐式参数类型)和方法名,获得所有可能被调用的候选方法。(注:有可能存在着方法的重载,即有多个参数类型不一样的方法)编译器会列举Son类中所有名为f的方法和其超类中所有的非私有的名为f方法,(超类的私有方法不可访问)
2.编译器查看调用方法时提供的参数类型。获得方法名字和参数类型。如果在所有名为f的方法中存在一个方法的参数类型与调用方法提供的实参类型匹配,就选择这个方法,这个过程称为重载解析。这个过程是比较复杂的,如要考虑到参数类型转换,如int 转换为double,Son转换为Father。如果编译器没有找到与参数类型相匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。
静态绑定:如果是private方法、static方法、final方法或者构造器。那么编译器将可以准确的知道应该调用那个方法,我们将这种调用方式称为静态绑定。
动态绑定:调用的方法依赖于隐式参数的实际类型,并且在运行时实现调用那个方法,我们称这种调用方式为动态绑定。
当程序运行,并采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适那个类的方法,假设x的实际类型是Son,它是Father的子类,如果Son类定义了方法f(String),就直接调用它,否则将在Son类的超类Father中寻找f(String),依次类推
方法表(method table):每次调用方法都要进行搜索,时间开销非常大,因此,虚拟机事先为每个类创建了一个方法表(method table)。其中列出了所有方法的签名和实际调用的方法。这样一来,在真正的调用方法的时候,虚拟机仅查找这个表就行了。
完整的过程;
Father s=new Father的子类(); //这里Son还可以替换为其他Father的子类
s.show();
1.编译器查看s的声明类型为Father
2.编译器将Father类方法表和其超类中所有的名为show的方法列举出来
3.编译器查看调用方法时提供的参数类型。进行重载解析,如果在所有名为show的方法中存在一个与提供的参数类型完全匹配, 就选择这个方法。至此这个方法就已经找到了。
4.判断该方法是静态绑定还是动态绑定。
如果该方法是private方法、final方法或构造器,java虚拟机就调用这个方法。调用方法结束。
否则,在程序运行过程中,虚拟机提取s引用对象的实际类型的方法表。有可能是Son的方法表,也有可能是Father其他子类的方法表
5.假设s引用对象的实际类型是Son.
如果Son类定义了该方法,虚拟机就直接调用它。
否则在Son类的超类中寻找show()方法,依次类推。