多态polymorphism
1、 案例引入
定义一个Animal类
public class Animal {
//String name = "animal";
public void eat(){
System.out.println("动物吃饭");
}
}
定义一个Dog类
public class Dog extends Animal {//继承Animal类
//String name = "dog";
@Override
public void eat() {
System.out.println("狗啃骨头");
}
}
定义一个Person类
public class Person {
//喂狗吃饭
public void feed(Dog dog){
dog.eat();
}
}
测试类
public class Test1 {
public static void main(String[] args) {
Person p=new Person();
//测试人喂狗吃饭
p.feed(new Dog());
}
}
这时如果需要拓展功能,增加一个猫Cat类,让人喂Cat吃饭,应该如何做?
很简单,在Person类中增加喂猫的方法,那如果后期还要不断的增加新功能(新增各种动物类),就要不断增加Person类对应的方法。这里就出现了一个问题:违背了软件开发中的开闭原则(对修改关闭,对扩展开放),并且大量类同代码冗余,即每次新增的方法除了参数类型不同,其他都基本相同。
能不能用简单方式解决?可以。
只需要在Person类中定义一个类似的方法,参数为Animal,其他全舍去,这就是多态的应用。
//修改后的Person类
public class Person {
//喂动物吃饭
public void feed(Animal animal){
animal.eat();
}
}
2、什么是多态
- 多态的形式
父类类型 变量名 = 子类对象;
父类类型:指子类继承的父类类型,或者实现的父接口类型。
所以说继承是多态的前提
//多态的引用形式
Animal a = new Cat();//Cat对象也属于其父类Animal类型。猫也是一个动物。
- 多态的表现
多态引用形式的具体表现:编译时类型与运行时类型不一致,编译时看左边的“父类”,父类必须包含要运行的方法,运行时看右边的“子类”。
也就是我们常说的:编译看左边,运行看右边。
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Animal a = new Cat();
// 编译时,a变量呈现Animal类型特征,即Animal类中有eat方法,a调用eat方法时编译才能成功
a.eat();//运行时,实际执行的是Cat类中重写的eat方法。
//a.catchMouse();//错误,编译时左边父类Animal中没有此方法,所以编译失败
// 多态形式,创建对象
a = new Dog();
//运行时执行的是 Dog类中重写的eat方法
a.eat();
}
}
- 多态的好处
运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法;变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
- 提高程序的扩展性
- 降低类与类之间的耦合度
3、多态的应用形式
1、多态应用在成员变量和方法参数
- 方法的形参是父类类型,调用方法的实参是子类对象
- 实例变量声明父类类型,实际存储的是子类对象
public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
this.pet = pet;
}
public void feed(){
pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
}
Dog dog = new Dog();
person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
person.feed();
Cat cat = new Cat();
person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
person.feed();
2、多态应用在数组
数组元素类型声明为父类类型,实际存储的是子类对象
private Pet[] pets;//数组元素类型是父类类型,元素存储的是子类对象
for (int i = 0; i < pets.length; i++) {
pets[i].eat();//pets[i]实际引用的对象类型不同,执行的eat方法也不同
}
pets[0] = new Dog();//多态引用
pets[1] = new Cat();//多态引用
3、多态应用在方法的返回值类型
//返回值类型是父类类型,实际返回的是子类对象
public Pet sale(String type){
switch (type){
case "Dog":
return new Dog();
case "Cat":
return new Cat();
}
return null;
}
4、向上转型与向下转型
1、类型转换
向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
- 此时,一定是安全的,而且也是自动完成的
向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
2、如何向上转型与向下转型
向上转型:自动完成
向下转型:(子类类型)父类变量
//没有类型转换
Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
//向上转型
Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
pet.setNickname("小白");
pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
// pet.watchHouse();//不能调用父类没有的方法watchHouse
Dog d = (Dog) pet;
d.eat();//可以调用eat方法
d.watchHouse();//可以调用子类扩展的方法watchHouse
Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
//这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
3、instanceof关键字
instanceof
关键字用于判断一个对象的运行时类型
为了避免ClassCastException
的发生,Java提供了 instanceof
关键字,给引用变量做类型的校验,只要用instanceof
判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException
异常。
格式:
变量/匿名对象 instanceof 数据类型
那么,哪些instanceof
判断会返回true呢?
- 变量/匿名对象的编译时类型 与
instanceof
后面数据类型是直系亲属关系才可以比较 - 变量/匿名对象的运行时类型<= instanceof后面数据类型,才为true