多态
向上转型
Dog dog = new Dog("肉球");
可以写为
Animal dog = new Dog("肉球");
此时dog是一个父类(Animal)的引用,指向一个子类(Dog)的实例,这种写法称为向上转型。
向上转型发生的时机:
●直接赋值
●方法传参
●方法返回
方法传参
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("肉球");
feed(dog);
}
public static void feed(Animal animal) {
animal.eat("火腿肠");
}
}
此时形参animal的类型是Animal(基类),实际上对应到Dog(派生类)的实例。
方法返回
public class Test {
public static void main(String[] args) {
Animal animal = findMyAnimal();
}
public static Animal findMyAnimal() {
Dog dog = new Dog("肉球");
return dog;
}
}
此时方法findMyAnimal返回的是一个Animal类型的引用,但是实际上对应到Dog的实例。
动态绑定
当子类和父类中出现同名的方法时,再去调用同名方法
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一只小动物");
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
public void eat(String food) {
System.out.println("我是一只小狗");
System.out.println(this.name + "正在吃" + food);
}
}
// Test.java
public class Test {
public static void main(String[] args) {
Animal animal1 = new Animal("板凳");
animal1.eat("食物");
Animal animal2 = new Dog("肉球");
animal2.eat("狗粮");
}
}
// 执行结果
我是一只小动物
板凳正在吃食物
我是一只小狗
肉球正在吃狗粮
此时:
animal1和animal2虽然都是Animal类型的引用,但是animal1指向Animal类型的实例,animal2指向Dog类型的实例。
针对animal1和animal2分别调用eat方法,发现animal.eat()实际调用了父类的方法,而animal2.eat()实际调用了子类的方法。
因此,在Java中,调用某个类的方法,究竟执行了哪段代码,要看究竟这个引用指向的是父类对象还是子类对象,这个过程是程序运行时决定的(而不是编译期),因此称为动态绑定。
方法重写
针对eat方法来说:
子类实现父类的同名方法,并且参数的类型和个数完全相同,这种情况称为覆写/重写/覆盖(Override)。
关于重写的注意事项
1.重写和重载是完全不同的东西
2.普通方法可以重写,static修饰的静态方法不能重写。
3.重写中子类的方法的访问权限不能低于父类的方法访问权限。
4.一般重写的方法返回值类型和父类的方法相同,特殊情况是:返回值类型为协变类型。
针对重写的方法,可以使用@Override注解来显示指定
// Bird.java
public class Dog extends Animal {
@Override
private void eat(String food) {
...
}
}
这个注解可以帮我们进行合法性校验,当未构成重写时会编译报错。
No | 区别 | 重载 | 重写 |
---|---|---|---|
1 | 概念 | 方法名称相同,参数的类型及个数不同 | 方法名称、返回值类型、参数的类型及个数完全相同 |
2 | 范围 | 一个类 | 继承关系 |
3 | 限制 | 没有权限要求 | 被重写的方法不能拥有比父类更严格的访问控制权限 |
理解多态
class Shape {
public void draw() {
// 啥都不用干
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("○");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("□");
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("♣");
}
}
/我是分割线//
// Test.java
public class Test {
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawMap(shape1);
drawMap(shape2);
drawMap(shape3);
}
// 打印单个图形
public static void drawShape(Shape shape) {
shape.draw();
}
}
在这个代码中分割线上方的代码是类的实现者写的,分割线下方的代码是类的调用者写的。
当类的调用者在编写drawMap这个方法的时候。参数的类型为Shape(父类),此时在该方法内部并不知道,也不关注当前的shape引用指向的是哪个类型的实力。此时shape这个引用调用draw方法可能会有多种不同的表现,这种行为就成为多态。
使用多态的好处
(1)类调用者对类的使用成本进一步降低。
封装是让类的调用者不需要知道类的实现细节。
多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可。
(2)能够降低代码的“圈复杂度”,避免使用大量的if-else。
(3)可扩展能力更强。
如果要新增一种新的形状,使用多态的方式代码改动的成本较低。
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
对于类的调用者来说(drawShapes方法),只要创建一个新类的实例就可以了。
向下转型
向下转型就是父类对象转变为子类对象,相比于向上转型来说,向下转型不常见,但有一定的用途。
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一只小动物");
System.out.println(this.name + "正在吃" + food);
}
}
// Dog.java
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
public void eat(String food) {
System.out.println("我是一只小狗");
System.out.println(this.name + "正在吃" + food);
}
public void run() {
System.out.println(this.name + "正在跑");
}
}
Animal animal = new Dog("肉球");
animal.eat("狗粮");
// 执行结果
肉球正在吃狗粮
尝试让肉球跑起来
animal.run();
// 编译出错
找不到 run 方法
注意事项
编译过程中,animal的类型是Animal,此时编译器只知道这个类中有一个eat方法,没有run方法。虽然animal实际引用的是一个Dog对象,但是编译器是以animal的类型来查看有哪些方法的。
对于Animal animal = new Dog(“肉球”); 这样的代码
●编译器检查有哪些方法存在,看的是Animal这个类型
●执行时究竟执行父类的方法还是子类的方法,看的是Dog这个类型
想要实现刚才的效果,就需要向下转型
// (Bird) 表示强制类型转换
Dog dog = (Dog)animal;
dog.run();
// 执行结果
肉球正在跑
但这样的向下转型是不太可靠的,如
Animal animal = new Snake("蛇");
Dog dog = (Dog)animal;
dog.run();
// 执行结果, 抛出异常
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird
at Test.main(Test.java:35)
animal本质上引用的是一个Snake对象,是不能转成Dog对象的,运行就会抛出异常。
所以为了让向下转型更安全,可以先判断animal本质是不是一个Dog实例。
Animal animal = new Snake("蛇");
if (animal instanceof Dog) {
Dog dog = (Dog)animal;
dog.run();
}
super关键字
如果需要在子类内部调用父类方法怎么办?可以使用super关键字。
super表示获取到父类实例的引用:
(1)使用super来调用父类构造器
public Dog(String name) {
super(name);
}
(2)使用super来调用父类的普通方法
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void eat(String food) {
// 修改代码, 让子调用父类的接口.
super.eat(food);
System.out.println("我是一只小狗");
System.out.println(this.name + "正在吃" + food);
}
}
如果在子类的eat方法中直接调用eat,那么此时是调用子类自己的eat方法,而加上super关键字,才是调用父类的方法。
No | 区别 | this | super |
---|---|---|---|
1 | 概念 | 访问本类中的属性和方法 | 由子类访问父类中的属性和方法 |
2 | 查找范围 | 先查找本类,如果没有就调用父类 | 不查找本类而直接调用父类 |
3 | 特殊 | 表示当前对象 | 无 |
如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系.
C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
Go 语言中没有 “继承” 这样的概念, 同样也能表示多态.
无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式.