目录
🍹欢迎各路大佬来到 Nick 主页指点
☀️本期文章将学习 [JavaSE] 面向对象编程(多态深度理解、向下转型、super&&this、大坑),我是博主Nick。✨
✨我的博客主页:Nick_Bears 🌹꧔ꦿ
🌹꧔ꦿ博文内容如对您有所帮助,还请给个点赞 + 关注 + 收藏✨
🔴 理解多态
📝 代码示例
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("♣"); } } //====================我的是华丽的分割线=================== 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); drawMap(new Flower()); drawMap(new Cycle()); drawMap(new Rect()); } // 打印单个图形 public static void drawMap(Shape shape) { shape.draw(); } }
🔶 在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的.
🔶 当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现 (和 shape 对应的实例相关), 这种行为就称为 多态
🔵 使用多态的好处是什么?
🍍 类调用者对类的使用成本进一步降低
- 封装是让类的调用者不需要知道类的实现细节.
- 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
🔶 因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低. 这也贴合了 > 中关于 "管理代码复杂程度" 的初衷
🍍 能够降低代码的 "圈复杂度", 避免使用大量的 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 .
🍍 可扩展能力更强
🔶 如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高。
🔵 向下转型
🔶 向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见, 但是也有一定的用途
// 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); } public void fly() { System.out.println(this.name + "正在飞"); } }
🔸 接下来是我们熟悉的操作
Animal animal = new Bird("圆圆"); animal.eat("谷子"); // 执行结果 圆圆正在吃谷子
🔸 接下来我们尝试让圆圆飞起来
animal.fly(); // 编译出错 找不到 fly 方法
🔻注意事项
🔷 编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法. 虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的. 对于 Animal animal = new Bird("圆圆") 这样的代码
- 编译器检查有哪些方法存在, 看的是 Animal 这个类型
- 执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型
🔹 那么想实现刚才的效果, 就需要向下转型
// (Bird) 表示强制类型转换 Bird bird = (Bird)animal; bird.fly(); // 执行结果 圆圆正在飞
🔹 但是这样的向下转型有时是不太可靠的. 例如
Animal animal = new Cat("小猫"); Bird bird = (Bird)animal; bird.fly(); // 执行结果, 抛出异常 Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird at Test.main(Test.java:35)
🔷 animal 本质上引用的是一个Cat 对象, 是不能转成 Bird 对象的. 运行时就会抛出异常,所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换
Animal animal = new Cat("小猫"); if (animal instanceof Bird) { Bird bird = (Bird)animal; bird.fly(); }
🔹 instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.
综合案例
class Animal { protected String name; public Animal(String name) { this.name = name; } public void eat() { } } class Bird extends Animal { public Bird(String name) { super(name); } @Override public void eat() { System.out.println("我是一只小鸟,"); System.out.println("我的名字叫"+this.name+",喜欢吃谷子!"); } void fly(){ System.out.println(this.name+"的飞行技能一流哦~"); } } class Cat extends Animal { public Cat(String name) { super(name); } @Override public void eat() { System.out.println("我是一只小猫,"); System.out.println("我的名字叫"+this.name+",喜欢吃小鱼!"); } } public class Demo { public static void main(String[] args) { animalManager(new Bird("小红")); System.out.println("==========华丽分割线=========="); animalManager(new Cat("小米")); } public static void animalManager(Animal animal) { if(animal instanceof Bird){ Bird bird=(Bird)animal; bird.eat(); bird.fly(); }else{ animal.eat(); } } }
🔵 super 关键字
🔶 前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用 super 关键字
💠 super 表示获取到父类实例的引用. 涉及到两种常见用法
🔸 使用了 super 来调用父类的构造器(这个代码前面已经写过了)
public Bird(String name) { super(name); }
🔸 使用 super 来调用父类的普通方法
public class Bird extends Animal { public Bird(String name) { super(name); } @Override public void eat(String food) { // 修改代码, 让子调用父类的接口. super.eat(food); System.out.println("我是一只小鸟"); System.out.println(this.name + "正在吃" + food); } }
🔸 在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了). 而加上 super 关键字, 才是调用父类的方法
🔻注意
🔸 super 和 this 功能有些相似, 但是还是要注意其中的区别
🔵 在构造方法中调用重写的方法(一个坑)
🔷 一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
class B { public B() { // do nothing func(); } public void func() { System.out.println("B.func()"); } } class D extends B { private int num = 1; @Override public void func() { System.out.println("D.func() " + num); } } public class Test { public static void main(String[] args) { D d = new D(); } } // 执行结果 D.func() 0
- 构造 D 对象的同时, 会调用 B 的构造方法.
- B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
- 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.
结论: 用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发 动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.
🔴 总结
🍒 多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带 来的编码上的好处
🍒 另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 "继承" 这样的语法并没有必然的联系
- C++ 中的 "动态多态" 和 Java 的多态类似. 但是 C++ 还有一种 "静态多态"(模板), 就和继承体系没有关系了.
- Python 中的多态体现的是 "鸭子类型", 也和继承体系没有关系.
- Go 语言中没有 "继承" 这样的概念, 同样也能表示多态.
🍒 无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式