多态
1 基本思想
用一个图形类来说明多态的基本思想,每个图形都拥有绘制自己的能力,这种能力可以看做是该类具有的行为,如果将子类的对象看做是父类的实例对象,这样当绘制图形时,简单地调用父类也就是图形类绘制图形的方法即可绘制任何图形,这就是多态的思想,总结为一句话就是将父类的对象应用于子类的特征就是多态,多态类的实现并不依赖于具体类,而是依赖于抽象类(父类)和接口.
2 向上转型
2.1 为何叫向上转型
在面向对象程序设计中,针对一些复杂的场景,我们通常画一个UML图来表示各个类之间的关系,通常父类画在子类的上方,因此我们就称之为"向上转型",表示往父类的方向转.
向上转型发生的时机:
- 直接赋值;
- 方法传参;
- 方法返回.
2.2 理解向上转型
我们先来看以下代码(以下代码为上文代码中的一部分):
猫咪本身是一种动物,cat是一个父类(Animal)的引用,指向的是一个子类(Cat)的实例,这种写法称之为向上转型.
3 动态绑定
问题来了!!当子类和父类中出现同名方法的时候,再去调用会出现什么情况呢???
示例如下:
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);
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
@Override
public void eat(String food) {
System.out.println("我是一只小鸟!");
System.out.println(this.name + "正在吃" + food);
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal1 = new Animal("麻雀");
animal1.eat("小米");
Animal animal2 = new Bird("鹦鹉");
animal2.eat("大米");
}
}
运行结果:
解读:
- animal1和animal2虽然都是Animal类型的引用,但是animal1指向Animal类型的实例,animal2指向Bird类型的实例;
- 针对animal1和animal2分别调用eat方法,发现animal1.eat()实际调用了父类的方法,而animal2.eat()实际调用了子类的方法.
在Java中,调用某个类的方法,究竟执行了哪段代码(是执行的父类方法啊还是执行的子类方法啊),要看引用指向的是父类还是子类对象,这个过程是程序运行时决定的,而不是在编译的时候决定的,因此称之为动态绑定.
4 方法重写与重载
针对上文刚写的eat方法来说,子类实现父类的重名方法,并且参数的类型和个数完全相同,这种情况我们称之为重写/覆写/覆盖(Override).
4.1 注意事项
- 普通方法可以重写,static修饰的静态方法不能重写;
- 重写中子类的方法的访问权限不能低于父类的方法的访问权限;
- 重写的方法返回值类型不一定和父类的方法相同(在这里我们建议写成相同);
- 重写和重载不要混淆,这是两种不同的概念,下文中将进行区分解释.
有关访问权限的示例如下:
4.2 方法的重载
同一个方法名字,提供不同版本的实现,称之为方法重载.
重载规则:
- 针对的是同一个类;
- 方法名相同;
- 方法的参数不同(参数个数或者参数类型);
- 方法的返回类型不影响重载.
示例如下:
public class TestDemo {
public static int add (int x,int y) {
return x + y;
}
public static double add(double x,double y) {
return x + y;
}
public static double add(double x,double y,double z) {
return x + y + z;
}
public static void main(String[] args) {
int a1 = 10;
int b1 = 20;
int ret1 = add(a1,b1);
System.out.println("ret1 = " + ret1 );
double a2 = 10.5;
double b2 = 20.2;
double ret2 = add(a2,b2);
System.out.println("ret2 = " + ret2);
double a3 = 10.5;
double b3 = 20.2;
double c3 = 30.1;
double ret3 = add(a3,b3,c3);
System.out.println("ret3 = " + ret3);
}
}
运行结果:
解读 : 方法的名字都叫add,但是有的add是计算int相加,有的是double相加;有的是两个数字相加,有的是三个数字相加;提供了同一个add方法名字,但是又不同的版本实现,这就是重载.
4.3 总结重载和重写的区别(*)
5 正式进入多态学习(*)
5.1 示例
通过下面的代码来体会一下多态的写法:
class Shape {
public void draw() {} //啥也不干
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("画了一个○");
}
}
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("画了一个□");
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("画了一朵小红花❀");
}
}
public class TestDemo {
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Shape shape1 = new Cycle();
Shape shape2 = new Rectangle ();
Shape shape3 = new Flower();
drawMap(shape1);
drawMap(shape2);
drawMap(shape3);
/*shape1.draw();
shape2.draw();
shape3.draw();*/
}
}
运行结果:
代码解读: 可以这样理解,Shape就相当于一支画笔,我们用它来画了圆/正方形/小红花;当类的调用者在编写drawMap这个方法的时候,参数类型为Shape(父类),此时在该方法内部是不知道的,也不关注当前的shape引用指向的是哪个类型/子类的实例,此时shape这个引用调用draw方法可能会有多种不同的表现(和shape对应的实例相关),这种行为就称之为多态.
核心思想: 一个引用,能表现出多种不同的形态;一个引用到底是指向父类对象还是某个子类对象(可能有多个),要根据代码来确定.
5.2 使用多态的好处
- 类调用者对类的使用成本进一步降低;
+封装让类的调用者不需要知道类的实现细节;
+多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可;
+可以理解为多态是封装的更进一步. - 能够降低代码的"圈复杂度",避免使用大量的if-else;
+圈复杂度是一种描述一段代码复杂程度的方式,简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称之为圈复杂度,如果一段代码中有许多条件分支和循环语句,我们理解起来也比较复杂. - 可扩展能力更强.
+对于类的调用者来说,只要创建一个新的类的实例就可以了,改动成本很低.
6 向下转型
6.1 背景概念
由上文我们可以知道向上转型是子类对象转成父类对象,那么向下转型便是父类对象转成子类对象,也就是说子类对象总是父类的一个实例,但父类对象不一定是子类的实例,相比于向上转型,向下转型不是很常见.
学习向下转型,我们还要知道另外一个概念:显示类型转换.
显示类型转换:越是具体的对象,具有的特性越多;越是抽象的对象,具有的特性越少.在做向下转型操作时,将特性范围小的对象转换为特性范围大的对象肯定会出现问题,所以这时候必须得告知编译器某些特性,将父类对象强制转换为某个子类对象,这便是显示类型转换.
6.2 问题示例
我们将上文中动态绑定中的部分代码稍作修改,如下所示:
代码解读: 编译过程中,animal类型是Animal,此时编译器只知道这个类中有一个eat方法,没有fly方法,虽然animal实际引用的是一个Bird对象,但是编译器是以animal的类型来查看有哪些方法的,针对
Animal animal = new Bird(“麻雀”)
这样的代码:
- 编译器检查有哪些方法存在时,看的是Animal这个类型;
- 执行时究竟执行的是父类的方法还是子类的方法,则看的是Bird这个类型.
如果我们要想实现fly()这个方法,该如何做呢???
方法一: 如果将animal进行强制类型转换可以么?? 我们来试一下:
public static void main(String[] args) {
Animal animal = new Bird("麻雀");
animal.eat("小米");
Bird bird = (Bird) animal;
bird.fly();
}
运行结果:
虽然已经运行成功了,但是总觉得有些不靠谱,如下代码:
public static void main(String[] args) {
Animal animal = new Cat("猫咪");
Bird bird = (Bird)animal;
bird.fly();
}
运行结果:
因为animal本质上引用的是一个Cat对象,是不能转成Bird对象的,因此就抛出了异常,所以这样的向下转型有局限性,是行不通的.
方法二: 为了避免在向下转型时抛出异常,我们可以先判定一下animal本质上是不是一个Bird实例,然后再进行转换,代码如下:
public static void main(String[] args) {
Animal animal = new Cat("猫咪");
if (animal instanceof Bird) {
Bird bird = (Bird)animal;
bird.fly();
}
}
这时候就不会抛出异常!!
instanceof可以判定一个引用是否是某个类的实例,如果是返回true.
在Java语言中的关键字都是小写!
7 super关键字
7.1 背景信息
上面的代码由于使用了重写机制,所以调用到的是子类的方法,那么如果要在子类内部调用父类方法怎么办? super关键字就是来解决这个问题的!
7.2 super关键字的使用
1)使用super来调用父类的构造器
class Cat extends demo6.Animal {
public Cat(String name) {
super(name);
}
}
2)使用super来调用父类的普通方法
说明: 如果在子类的eat方法中直接调用eat,那么此时就认为是调用的自己的eat,也就是递归了.
注意点: 尽量不要在构造器中调用方法,如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没有构造完成,可能会出现一些隐藏的并且极难发现的问题.