• Java_多态


     作者:爱塔居的博客_CSDN博客-JavaSE领域博主

    专栏:JavaSE

    作者简介:大三学生,希望跟大家一起进步

    一、多态概念

    多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会生出不同的状态。
    比如:动物吃东西
    狗吃狗粮:

    猫吃猫粮:

     再比如学习:

    有些小猫咪学习:

    再有些小猫咪学习:

    总的来说:同一件事情,发生在不同对象身上就会产生不同的结果。

     二、多态实现条件

    🥝在java中要实现多态,必须要满足如下几个条件,缺一不可:

    1.必须在继承体系下

    2.子类必须要对父类中方法进行重写

    3.通过父类的引用调用重写的方法(向上转型

    多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法

    1. class Animal {
    2. String name;
    3. int age;
    4. public Animal(String name, int age){
    5. this.name = name;
    6. this.age = age;
    7. }
    8. public void eat(){
    9. System.out.println(name + "吃饭");
    10. }
    11. }
    12. class Cat extends Animal{
    13. public Cat(String name, int age){
    14. super(name, age);
    15. }
    16. @Override
    17. public void eat(){
    18. System.out.println(name+"吃鱼~~~");
    19. }
    20. }
    21. class Dog extends Animal {
    22. public Dog(String name, int age){
    23. super(name, age);
    24. }
    25. @Override
    26. public void eat(){
    27. System.out.println(name+"吃骨头~~~");
    28. }
    29. }
    30. public class Test {
    31. public static void eat(Animal a){
    32. a.eat();
    33. }
    34. public static void main(String[] args) {
    35. Cat cat = new Cat("元宝",2);
    36. Dog dog = new Dog("小七", 1);
    37. eat(cat);
    38. eat(dog);
    39. }
    40. }

    输出结果:

     当类的调用者在编写eat这个方法时,参数类型为Animal(父类),此时在该方法内部并不知道,也不关注当前的a引用指向的时哪个类型(哪个子类)的实例。此时a这个引用调用eat方法可能会有多种不同的表现。

    三、重写

    🧀重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰、非final修饰,非构造方法等的实现过程进行重新编写,返回值和形参都不能改变。

    🍨重写的好处:子类可以根据需要,定义特定于自己的行为。

    【方法重写的规则】

    1.子类在重写父类的方法时,一般必须与父类方法原型一致:返回值 方法名 (参数列表)要完全一致

    2.被重写的方法返回值可以不同,但是必须时具有父子关系的

    3.访问权限不能比父类中被重写的方法的访问权限更低。比如:如果父类方法被public修饰,则子类中重写该方法就不能声明为protected

    4.重写的方法,可以使用@Override注解来显式指定。有了这个注解,就能帮我们进行一些合法性校验。

    当比如,我们把子类的方法名字故意改成eat1,则会出现下面的编译错误。 

    【重写和重载的区别】

     🌾方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现

    方法重载的参数是不一样的。方法重写的参数是一样的。

    【重写的设计原则】

    对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
    就比如手机,之前的手机打电话发短信,只是来电显示,显示号码,而现在的手机会显示号码、地区】姓名。而,之前的手机现在还有用户在使用,所以我们不能直接在那个基础上改,而是重新定义一个类,也就是重写。
    静态绑定 :也称为前期绑定 ( 早绑定 ) ,即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
    动态绑定 :也称为后期绑定 ( 晚绑定 ) ,即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。

     🥙动态绑定的条件:

    1.发生向上转型

    2.发生重写

    3.通过父类引用,调用这个父类和子类重写的方法

    我们举这个代码为例: 

    1. class Animal{
    2. String name;
    3. public void eat(){
    4. System.out.println(name+"吃饭");
    5. }
    6. }
    7. class Cat extends Animal{
    8. public void eat(){
    9. System.out.println(name+"吃猫粮");
    10. }
    11. }
    12. class Dog extends Animal{
    13. public void eat(){
    14. System.out.println(name+"吃狗粮");
    15. }
    16. }
    17. public class Test {
    18. public static void eat(Animal a){
    19. a.eat();
    20. }
    21. public static void main(String[] args) {
    22. Animal animal1=new Dog();
    23. animal1.name="小汪";
    24. animal1.eat();
    25. Animal animal2=new Cat();
    26. animal2.name="小喵";
    27. animal2.eat();
    28. }
    29. }

     在代码界面右键,点击Open In,选择exploer,进入该代码所在的文件下,然后寻找该代码的out文件:

     选择:

     接着,我们在这里:

     删掉内容,输入 cmd,回车:

     跳出以上页面。输入

    javap -c Test

    回车生成:

    我们会注意到:

     

     这证明了,动态绑定需要等到程序运行时,才能够确定具体调用那个类的方法。

    四、向上转型和向下转型

    4.1 向上转型

    🎂向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。

     语法格式:父类类型 对象名 = new 子类类型()

    Animal animal = new Cat ( " 小喵 " , 2 );

    使用场景

    1.直接赋值

    2.方法传参

    3.方法返回

    1. class Animal{
    2. String name;
    3. public void eat(){
    4. System.out.println(name+"吃饭");
    5. }
    6. }
    7. class Dog extends Animal{
    8. public void eat(){
    9. System.out.println(name+"吃狗粮");
    10. }
    11. }
    12. public class Test {
    13. public static void eat(Animal a){
    14. a.eat();
    15. }
    16. public static void main(String[] args) {
    17. Animal animal1=new Dog();
    18. animal1.name="小汪";
    19. animal1.eat();
    20. }
    21. }

    其中:

    为向上转型。 

    向上转型的优点:让代码实现更简单灵活。
    向上转型的缺陷:不能调用到子类特有的方法。

    4.2 向下转型

    🍐将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。

    👇为向下转型:

    向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。

    1. class Animal{
    2. String name;
    3. public void eat(){
    4. System.out.println(name+"吃饭");
    5. }
    6. }
    7. class Dog extends Animal{
    8. public void eat(){
    9. System.out.println(name+"吃狗粮");
    10. }
    11. }
    12. public class Test {
    13. public static void eat(Animal a){
    14. a.eat();
    15. }
    16. public static void main(String[] args) {
    17. Animal animal1=new Dog();
    18. if(animal1 instanceof Dog){
    19. Dog dog=(Dog)animal1;
    20. animal1.name="小汪";
    21. animal1.eat();
    22. }
    23. }
    24. }

    五、多态的优缺点 

    💰使用多态的优点:

    1.能够降低代码的“圈复杂度”,避免使用大量的if-else

    圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
    因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一个方法的圈复杂度太高, 就需要考虑重构. 不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10
    1. class DrawDraw{
    2. public void Draw(){
    3. System.out.println("画画");
    4. }
    5. }
    6. class Flower extends DrawDraw{
    7. public void Draw(){
    8. System.out.println("❀");
    9. }
    10. }
    11. class Star extends DrawDraw{
    12. public void Draw(){
    13. System.out.println("⭐");
    14. }
    15. }
    16. public class Test {
    17. public static void main(String[] args) {
    18. DrawDraw[] shapes = {new Star(), new Flower()};
    19. for (DrawDraw shape : shapes) {
    20. shape.Draw();
    21. }
    22. }
    23. }

    2.可扩展能力强 

    如果要新增一种新的形状 , 使用多态的方式代码改动成本也比较低
    比如在刚才的代码上加一个太阳的形状:
    1. class DrawDraw{
    2. public void Draw(){
    3. System.out.println("画画");
    4. }
    5. }
    6. class Flower extends DrawDraw{
    7. public void Draw(){
    8. System.out.println("❀");
    9. }
    10. }
    11. class Star extends DrawDraw{
    12. public void Draw(){
    13. System.out.println("⭐");
    14. }
    15. }
    16. class Sun extends DrawDraw{
    17. public void Draw(){
    18. System.out.println("☀");
    19. }
    20. }
    21. public class Test {
    22. public static void main(String[] args) {
    23. DrawDraw[] shapes = {new Star(), new Flower(),new Sun()};
    24. for (DrawDraw shape : shapes) {
    25. shape.Draw();
    26. }
    27. }
    28. }
    对于类的调用者来说 (drawShapes 方法 ), 只要创建一个新类的实例就可以了 , 改动成本很低 .
    而对于不用多态的情况 , 就要把  if - else 进行一定的修改 , 改动成本更高。
    🎨多态缺陷:
    1.属性没有多态性,当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
    2.构造方法没有多态性

    六、避免在构造方法中调用重写的方法

    一段有坑的代码 . 我们创建两个类 , B 是父类 , D 是子类 . D 中重写 func 方法 . 并且在 B 的构造方法中调用 func
    1. class B {
    2. public B() {
    3. // do nothing
    4. func();
    5. }
    6. public void func() {
    7. System.out.println("B.func()");
    8. }
    9. }
    10. class D extends B {
    11. private int num = 1;
    12. @Override
    13. public void func() {
    14. System.out.println("D.func() " + num);
    15. }
    16. }
    17. public class Test {
    18. public static void main(String[] args) {
    19. D d = new D();
    20. }
    21. }
    22. // 执行结果
    23. D.func() 0
    构造 D 对象的同时 , 会调用 B 的构造方法。 B 的构造方法中调用了 func 方法 , 此时会触发动态绑定 , 会调用到 D 中的 func 。此时 D 对象自身还没有构造 , 此时 num 处在未初始化的状态 , 值为 0. 如果具备多态性, num 的值应该是 1。所以在构造函数内,尽量避免使用实例方法,除了final private 方法。
    结论 : " 用尽量简单的方式使对象进入可工作状态 ", 尽量不要在构造器中调用方法 ( 如果这个方法被子类重写 , 就会触发动态绑定, 但是此时子类对象还没构造完成 ), 可能会出现一些隐藏的但是又极难发现的问题 .

  • 相关阅读:
    Netty入门指南之Reactor模型
    java基于Springboot+vued的医院急诊挂号预约系统 element
    【Linux专题】配置存储多路径(一)
    分布式 PostgreSQL 集群(Citus)官方安装指南
    WebRTC janus安装编译教程
    《机器学习分类器 二》——朴素的贝叶斯算法,项目实践,算法实践。
    ECharts合集
    「Python入门」Python代码规范(风格)
    Ubuntu写python脚本实现自定义壁纸幻灯片:字符串拼接法、minidom法
    postgresql-字符函数
  • 原文地址:https://blog.csdn.net/m0_65683419/article/details/127906237