目录
前言
根据前面继承的学习,我们以及了解到了继承是指子类继承父类的字段和方法,也知道了子类和父类的关系是(is ->a)的关系,在平时生活中我们就有猫是一只动物的说法,那么是不是在Java语言中也存在这么一种表现形式呢
一、多态的引入
好比之前继承的例子,设计一个类来表示动物
//Animal类
class Animal {
//定义动物的名字
protected String name;
//定义动物的有参构造方法
public Animal(String name) {
this.name = name;
}
//定义动物的无参构造方法
public Animal() {
}
//定义动物吃食物的方法
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
//Cat类
class Cat extends Animal{
//定义猫的名字
protected String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
//定义猫吃食物的方法
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
//定义猫跑的方法
public void run() {
System.out.println(this.name + "正在跑");
}
}
//Bird类 class Bird extends Animal { public Bird() { } public Bird(String name) { this.name = name; } //定义鸟飞的方法 public void fly() { System.out.println(this.name + "正在飞"); } }
按照我们之前类和对象的学习,创建一个对象的基本语法为
类名 变量名 = new 类名对应的构造方法;
Animal animal = new Animal ("小动物");
那么我们是不是也可以让父类对象,指向子类实例,就比如:
Animal animal = new Cat("喵喵");
上面这种现象在java语言中就被称之为多态
二、多态是什么
多态:将子类对象赋值给父类变量,即父类引用指向的是一个子类的实例(即子类对象)
就好比在形容一只猫你可以说它是一只动物一样
三、语法规则
基本语法
父类类名 变量名 = new 子类构造方法;
Animal animal = new Cat("喵喵");
四、向上转型
上面的例子中我们出现了形如Animal animal = new Cat("喵喵");的代码,它实则是发生了向上转型,我们可以通过is -> a语义来进行理解;
我们看到喵喵在吃猫粮,我就可以说喵喵在吃猫粮,当然我也可以说这里有只动物在吃猫粮,因为喵喵是一只猫,但它同时也是一只动物;
向上转型发生的时机
直接赋值
方法传参
方法返回
直接赋值的方式我们在上面已经演示了. 另外两种方式其实和直接赋值没有本质区别.
方法传参
此时形参animal 的类型是Animal (父类),实际上对应到Cat (子类) 的实例
方法返回
此时方法findMyAnimal 返回的是一个 Animal 类型的引用,但是实际上对应的是 Bird 的实例
这种情况类似于:
double num = 100; // 正确,int --> double,自动类型转换
五、向下转型
既然向上转型是子类对象转换为父类对象,所以向下转型就是父类对象转换成子类对象,相比于向上转型来说,向下转型就没有那么常见了,但是也是有它一定的用途的
接下来就是我们熟悉的例子了
按之前的讲解我们知道,这种现象是多态,既然咕咕是一只鸟,那么我们要是想让咕咕飞起来会发生什么呢?
这时我们发现,报错了,那么这是为什么呢?
注意事项
编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.
虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.
对于 Animal animal = new Bird("圆圆") 这样的代码,
编译器检查有哪些方法存在, 看的是 Animal 这个类型
执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.
所以我们要是想实现刚才的效果, 就需要向下转型.
虽然这种方法可以让父类转换为子类对象,但是这样的向下转型有时是不可靠的,就比如
诶,当我们将一只猫强制转换为鸟的时候就出现了ClassCastException异常,这个异常就是我们常常所说的类型转换异常,它指的是两种不可转换的类型之间发生了强制转换,那么这是为什么呢
animal 本质上引用的是一个Cat 对象,而Cat 对象是不能转换为 Bird 对象的,所以运行时就会抛出异常,那么我们有没有什么方法可以来判断转换是否能成功呢,所以我们引入了instanceof关键字
instanceof关键字
instanceof关键字是用于判定一个引用是否为某个类的实例,如果是那么将返回 true ,如果不是那么就返回 false 所以经过判断后在进行向下转型就比较安全了,所以之前的代码我们可以改成
这样的话我们就可以保证向下转型的正确性
六、动态绑定
之前的例子中我们子类和父类不存在同名方法,那么要是当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?
对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的标志.
//Animal类 class Animal { //定义动物的名字 protected String name; //定义动物的有参构造方法 public Animal(String name) { this.name = name; } //定义动物的无参构造方法 public Animal() { } //定义动物吃食物的方法 public void eat(String food) { System.out.println("我是一只小动物"); System.out.println(this.name + "正在吃" + food); } } //Bird类 class Bird extends Animal { public Bird() { } public Bird(String name) { this.name = name; } //定义鸟吃食物的方法 public void eat(String food) { System.out.println("我是一只小鸟"); System.out.println(this.name + "正在吃" + food); } //定义鸟飞的方法 public void fly() { System.out.println(this.name + "正在飞"); } }
此时,我们发现这样一个现象:
animalA 和animalB 虽然都是Animal 类型的引用,但是 animalA 指向了Animal 类型的实例,而 animalB 指向了 Bird 类型的实例
针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而animal2.eat() 实际调用了子类的方法.
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.
七、方法重写
针对刚刚动态绑定中的 eat 方法来说,子类实现了父类的同名方法,并且参数的类型和个数与父类完全相同,这种情况我们就把它称之为覆写/重写/覆盖(Override).
关于重写的注意事项
1. 重写和重载完全不一样. 不要混淆(可以思考一下, 重载的规则又是啥?)
2. 普通方法可以重写, static 修饰的静态方法不能重写.
3. 重写中子类的方法的访问权限不能低于父类的方法访问权限.
4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).
方法权限示例: 将子类的 eat 改成 private
另外,针对重写的方法,我们可以使用@Override 注解来显式指定.
有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
所以一般我们推荐在代码中进行重写方法时显式加上 @Override 注解.
八、总结
小结: 重载和重写的区别.