向上转型
概念
什么是向上转型,总的来说就是父类引用引用子类对象
向上转型发生的时机(三种)
那么什么时候会发生向上转型呢?我们来看,共有三种
直接赋值
1.class Animal { 2. public String name; 3. public Animal(String name) { 4. this.name = name; 5. } 6. public void eat() { 7. System.out.println(this.name + " Animal::eat()"); 8. } 9.} 10.class Cat extends Animal { 11. public Cat(String name) { 12. super(name);//显示调用父类的构造方法 13. } 14. public void drink() { 15. System.out.println("this.name" + "喝水"); 16. } 17.} 18.public class TestDemo{ 19. public static void main(String[] args) { 20. //直接赋值 21. Animal animal=new Cat("ddd"); 22. animal.eat(); 23. } 24.}
注意事项:
此时的父类引用只能访问自己的属性或者调用自己的方法,不能访问子类中特有的属性和调用子类中特有的方法(方法重写除外,涉及到运行时绑定)
例如上述代码animal这个引用是可以访问其Animal类中eat方法的,此时虽然子类Cat继承了Animal这个类,但是Cat类也是有自己特有的drink方法的,那么此时animal这个引用是不能调用drink方法的。
方法传参
1.class Animal { 2. public String name; 3. public Animal(String name) { 4. this.name = name; 5. } 6. public void eat() { 7. System.out.println(this.name + " Animal::eat()"); 8. } 9.} 10.class Cat extends Animal { 11. public Cat(String name) { 12. super(name);//显示调用父类的构造方法 13. } 14. public void drink() { 15. System.out.println("this.name" + "喝水"); 16. } 17.} 18.public class TestDemo{ 19. public static void func(Animal animal) { 20. animal.eat(); 21. } 22. public static void main(String[] args) { 23. //方法传参 24. Cat cat = new Cat("豆豆"); 25. func(cat); 26. } 27.}
此时形参 animal 的类型是 Animal (父类), 实际上对应到 Cat (子类) 的实例
方法返回
1.class Animal { 2. public String name; 3. public Animal(String name) { 4. this.name = name; 5. } 6. public void eat() { 7. System.out.println(this.name + " Animal::eat()"); 8. } 9.} 10.class Cat extends Animal { 11. public Cat(String name) { 12. super(name);//显示调用父类的构造方法 13. } 14. public void drink() { 15. System.out.println("this.name" + "喝水"); 16. } 17.} 18.public class TestDemo{ 19. public static Animal func() { 20. Cat cat = new Cat("豆豆"); 21. return cat; 22. 23. } 24. public static void main(String[] args) { 25. //返回值 26. Animal animal=func(); 27. animal.eat(); 28. } 29.}
此时方法 func 返回的是一个 Animal 类型的引用, 但是实际上对应到 Cat类的实例.
动态绑定(运行时绑定)
当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?
1.class Animal { 2. public String name; 3. public Animal(String name) { 4. this.name = name; 5. } 6. public void eat() { 7. System.out.println(this.name + " Animal::eat()"); 8. } 9.} 10.class Cat extends Animal { 11. public Cat(String name) { 12. super(name);//显示调用父类的构造方法 13. } 14. public void drink() { 15. System.out.println("this.name" + "喝水"); 16. } 17. @Override//注解代表重写 18. public void eat() { 19. System.out.println(this.name + "Cat::eat()"); 20. } 21.} 22. 23.public class TestDemo{ 24. public static void main(String[] args) { 25. //运行时绑定 26. Animal animal=new Cat("豆豆"); 27. animal.eat(); 28. } 29.}
此时最终的输出结果为豆豆Cat::eat(),我们会发现此时调用了子类Cat中的eat方法,并没有调用父类Animal中的eat方法,
那么这是为什么呢?
答:因为此时发生了运行时绑定:即当父类引用去引用子类对象且父类引用调用父类在子类中被重写的方法时会发生运行时绑定 当animal这个引用调用eat方法时,其在编译过程中调用的还是父类Animal中的eat方法,但是 在运行时调用的却是子类Cat中重写的eat方法,这便叫做运行时绑定 了解运行时绑定也是了解多态的前提.
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.
方法重写
什么是方法重写
1.返回值相同 2.方法名称相同 3.参数列表相同 4.方法的重写是建立在继承关系上的,即子类继承父类时可重写父类的方法 这种情况称为 覆写/重写/覆盖(Override).
注意事项(重要)
被重写的方法不能用final进行修饰,否则不能进行重写
被重写的方法不能被private修饰
在子类中重写的方法的访问修饰限定符的权限一定要大于等于父类中被重写的方法的权限
其遵循这样的规则:privated
被static所修饰的静态方法是不能被重写的
我们推荐在代码中进行重写方法时显式加上 @Override
重写方法时,抛出异常可以是父类方法抛出异常的全集,子集,空集。(动力节点的老师增加的,具体可以看课程)
重写的方法的返回值,可以缩小返回类型的权限范围,但是不能增加返回类型的权限范围,这个返回值之间的关系我们称之为协变类型
@Override 在 Java 中称为 “注解”,后面的章节中我们会详细讲到
示例:将子类的 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
另外, 针对重写的方法, 可以使用 @Override 注解来显式指定.
// Bird.java public class Bird extends Animal { @Override private void eat(String food) { ............. } }
有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
我们推荐在代码中进行重写方法时显式加上 @Override 注解.
关于注解的详细内容, 我们会在后面的章节再详细介绍
方法重载和方法重写的区别
理解多态
有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了. 我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况.
代码示例:打印多种形状
1.class Shape { 2. public void draw() { 3. System.out.println("打印图形"); 4. } 5.} 6. 7.class Cycle extends Shape { 8. @Override 9. public void draw() { 10. System.out.println("画一个□"); 11. } 12.} 13. 14.class React extends Cycle { 15. @Override//注解 16. public void draw() { 17. System.out.println("画一个□"); 18. } 19.} 20. 21.class Triangle extends Shape { 22. @Override//注解 23. public void draw() { 24. System.out.println("画一个); 25. } 26.} 27. 28. ---------------------------------------------分割线---------------------------------------------------- 29.public class TestDemo { 30. public static void fun(Shape shape) { 31. shape.draw(); 32. } 33. 34. public static void main(String[] args) { 35. 36. Shape shape = new Cycle(); 37. Shape shape2 = new React(); 38. Shape shape3 = new Triangle(); 39. fun(shape); 40. fun(shape2); 41. fun(shape3); 42. System.out.println("==============================="); 43. System.out.println("另一种写法,与上面代码输出结果相同"); 44. Shape[] shapes = {new Cycle(), new React(), new Triangle()}; 45. for (Shape shape4 : shapes) { 46. shape4.draw(); 47. } 48. 49. } }
在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的.
当类的调用者在编写 fun这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例对象. 此时 shape 这个引用调用 fun方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态.
多态发生的前提
首先来看下多态发生的前提:
1:父类引用引用子类对象 2:父类子类拥有同名的覆写方法 3:父类引用此时调用这个覆写的方法
那么此时我们来看上面的代码,此时首先我们写了向上转型的代码,用父类引用此时指向了子类对象,并且在子类中我们重写了父类的方法
当类的调用者在编写fun这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道,
也不关注当前的 shape引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用fun
方法可能会有多种不同的表现(和 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 进行一定的修改, 改动成本更高.
向下转型
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见, 但是也有一定的用途.
// 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 方法,fly方法只定义在Bird类中
注意事项:
编译过程中, animal 的类型是 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)
错误原因:错误的原因是Cat类中此时并没有Bird类中的fly方法,盲目的进行向下转型会发生错误
animal引用本质上引用的是一个 Cat 对象, 是不能转成 Bird 对象的. 运行时就会抛出ClassCastException异常.
所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换,所以此时便会引用instanceof关键字
Animal animal = new Cat("小猫"); //此处判断animal这个引用是否引用了Bird这个类的实例对象 if (animal instanceof Bird) { Bird bird = (Bird) animal; bird.fly(); } else { System.out.println("ERROR"); }
instanceof 可以判定一个引用是否是某个类的实例.同时也可理解为判断一个引用之前是否引用了某个类的实例如果是, 则返回 true. 这时再进行向下转型就比较安全了.
在构造方法中调用重写的方法(一个坑)
一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
总结
多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处.
另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系.
C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
Go 语言中没有 “继承” 这样的概念, 同样也能表示多态.
无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式.