多态(向上转型、动态绑定、方法重写、理解多态)

多态

向上转型
Bird bird = new Bird("圆圆");

这个代码也可以写成这个样子

Bird bird = new Bird("圆圆");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");

此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为向上转型.
在这里插入图片描述

向上转型之后 通过父类的引用 只能访问父类自己的方法或者属性

向上转型发生的时机:
直接赋值
方法传参
方法返回

通过父类引用去访问的时候只能访问父类的方法或者属性,不能访问子类的方法或者属性
在这里插入图片描述

在这里插入图片描述
直接赋值的方式我们已经演示了. 另外两种方式和直接赋值没有本质区别.
方法传参

public class Test {
 public static void main(String[] args) {
 Bird bird = new Bird("圆圆");
 feed(bird);
 }
 public static void feed(Animal animal) {
 animal.eat("谷子");
 }
}
// 执行结果
//圆圆正在吃谷子

此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例

方法返回

public class Test {
 public static void main(String[] args) {
 Animal animal = findMyAnimal();
 }
 public static Animal findMyAnimal() {
 Bird bird = new Bird("圆圆");
 return bird;
 }
} 

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例.

动态绑定

当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?
对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志

// 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 Bird extends Animal {
 public Bird(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 Bird("扁扁");
 animal2.eat("谷子");
 }
}
// 执行结果
//我是一只小动物
//圆圆正在吃谷子
//我是一只小鸟
//扁扁正在吃谷子

此时, 我们发现:
animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向
Bird 类型的实例.
针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而
animal2.eat() 实际调用了子类的方法

动态绑定也叫运行时绑定指父类引用引用子类对象,同时通过父类引用调用同名的覆盖方法,此时就会发生运行时绑定。
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引
用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.

方法重写

针对刚才的 eat 方法来说:
子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).
关于重写的注意事项

  1. 重写和重载完全不一样. 不要混淆(思考一下, 重载的规则是啥?)
  2. 普通方法可以重写, static 修饰的静态方法不能重写
  3. 重写中子类的方法的访问权限不能低于父类的方法访问权限.
  4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).
    方法权限示例: 将子类的 eat 改成 private
// Animal.java
public class Animal {
 public void eat(String food) {
 ...
 }
}
// Bird.java
public class Bird extends Animal {
 // 将子类的 eat 改成 private
 private void eat(String food) {
 ...
 }
}
// 编译出错
//Error:(8, 10) java: com.bit.Bird中的eat(java.lang.String)无法覆盖com.bit.Animal中的
//eat(java.lang.String)
 //正在尝试分配更低的访问权限; 以前为public

体会动态绑定和方法重写
上面讲的动态绑定和方法重写是用的相同的代码示例.
事实上, 方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现. 两者本质上描述
的是相同的事情, 只是侧重点不同

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

理解多态

什么是多态:思想
1.父类引用 引用子类对象
2.父类和子类有同名的覆盖方法
3.通过父类引用 调用重写的方法的时候 发生了运行时绑定 称为多态
多态是一种思想

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.
我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况

代码示例: 打印多种形状

class Shape {
 public void draw() {
 // 啥都不用干
 }
}
class Cycle extends Shape {
 @Override  //为重写注解 ctrl+insert/ctrl+o自动生成方法   
 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 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态

多态顾名思义, 就是 “一个引用, 能表现出多种不同形态”
举个具体的例子. 家里养了两只鹦鹉(圆圆和扁扁)和一个小孩(核弹). 媳妇管他们都叫 “儿子”. 这时候我对我媳妇说, “你去喂喂你儿子去”. 那么如果这里的 “儿子” 指的是鹦鹉, 我媳妇就要喂鸟粮; 如果这里的 “儿子” 指的是核弹, 我媳妇就要喂馒头.
那么如何确定这里的 “儿子” 具体指的是啥? 那就是根据我和媳妇对话之间的 “上下文”.
代码中的多态也是如此. 一个引用到底是指向父类对象, 还是某个子类对象(可能有多个), 也是要根据上下文的代码来确定.

在这里插入图片描述
在这里插入图片描述
使用多态的好处是什么?
1) 类调用者对类的使用成本进一步降低.
封装是让类的调用者不需要知道类的实现细节.
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷
在这里插入图片描述
2) 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:

public static void drawShapes() {
 Rect rect = new Rect();
 Cycle cycle = new Cycle();
 Flower flower = new Flower();
 String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};

 for (String shape : shapes) {
 if (shape.equals("cycle")) {
 cycle.draw();
 } else if (shape.equals("rect")) {
 rect.draw();
 } else if (shape.equals("flower")) {
 flower.draw();
 }
 }
} 

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单

public static void drawShapes() {
 // 我们创建了一个 Shape 对象的数组.
 Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
 new Rect(), new Flower()};
 for (Shape shape : shapes) {
 shape.draw();
 }
}

什么叫 “圈复杂度” ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构
不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .

3) 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低

class Triangle extends Shape {
 @Override
 public void draw() {
 System.out.println("△");
 }
} 

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值