首页 > 技术文章 > 方法覆盖和多态

hty20010101 2021-11-29 19:08 原文

方法覆盖和多态

方法覆盖

1.什么时候考虑使用方法覆盖?

  • 父类中的方法无法满足子类的业务需求,子类有必要对继承过来的方法进行覆盖。

  • 方法覆盖又叫做:方法重写(重新编写),英语单词叫做:Override、Overwrite。

比较常见的:方法覆盖、方法重写、override

 

2.什么条件满足的时候构成方法覆盖?

第一:有 继承 关系的两个类

第二:具有 相同方法名、返回值类型、形式参数列表

第三:访问权限不能更低,,可以更高。

第四:抛出异常不能更多,可以更少。(原因)

访问权限修饰符权限从高到低排列是:public ,protected ,默认(就是不写) , private

 

这里还有几个注意事项:

注意1:方法覆盖只是针对于 方法,和属性无关。

注意2:私有方法 无法覆盖。

注意3:构造方法 不能被继承,所以 构造方法 也不能被覆盖。

注意4:方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。

 

3.关于Object类中toString()方法的覆盖?

  • toString()方法存在的作用就是:将java对象转换成字符串形式。

  • 大多数的java类toString()方法都是需要覆盖的。因为Object类中提供的toString()方法输出的是一个java对象的内存地址。

  • 至于toString()方法具体怎么进行覆盖?

格式可以自己定义,或者听需求的。(听项目要求的。)

 

4.方法重载和方法覆盖有什么区别?

4.1什么时候用?

  • 方法重载:当在一个类当中,如果功能相似的话,建议将名字定义的一样,这样代码美观,并且方便编程。

  • 方法覆盖:父类中的方法无法满足子类的业务需求,子类有必要对继承过来的方法进行覆盖。

 

4.2位置?

  • 方法重载:发生在同一个类当中。

  • 方法覆盖:是发生在具有继承关系的父子类之间。

 

4.3二者区别

  • 方法重载:是同一个类中,方法名相同,参数列表不同(个数、顺序、类型)。

  • 方法覆盖:是具有继承关系的父子类,并且重写之后的方法必须和之前的方法一致:方法名一致、参数列表一致、返回值类型一致。

 

重要结论:

当子类对父类继承过来的方法进行“方法覆盖”之后,子类对象调用该方法的时候,一定执行覆盖之后的方法。

 

注意:

最好将父类中的方法原封不动的复制过来。(不建议手动编写)。

方法覆盖,就是将继承过来的那个方法给覆盖掉了。继承过来的方法没了。

eg.

 class Animal{
    public void move(){
    System.out.println("动物在移动!");
    }
 
    public void sing(int i){
    System.out.println("Animal sing....");
    }
 }
 
 class Bird extends Animal{
    // 对move方法进行方法覆盖,方法重写,override
    // 最好将父类中的方法原封不动的复制过来。(不建议手动编写)
    // 方法覆盖,就是将继承过来的那个方法给覆盖掉了。继承过来的方法没了。
    public void move(){
    System.out.println("鸟儿在飞翔!!!");
    }
     
    //protected表示受保护的。没有public开放。
    // 错误:正在尝试分配更低的访问权限; 以前为public
    /*
    protected void move(){
        System.out.println("鸟儿在飞翔!!!");
    }
  */
 
    //错误:被覆盖的方法未抛出Exception
    /*
    public void move() throws Exception{
        System.out.println("鸟儿在飞翔!!!");
    }
  */
 
    // 分析:这个sing()和父类中的sing(int i)有没有构成方法覆盖呢?
    // 没有,原因是,这两个方法根本就是两个完全不同的方法。
    // 可以说这两个方法构成了方法重载吗?可以。
    public void sing(){
    System.out.println("Bird sing.....");
    }
 }

注意:

 // 分析:这个sing()和父类中的sing(int i)有没有构成方法覆盖呢?
 // 没有,原因是,这两个方法根本就是两个完全不同的方法。
 // 可以说这两个方法构成了方法重载吗?可以。
 Animal类的
 public void sing(int i){
  System.out.println("Animal sing....");
 }
 和Bird类的
 public void sing(){
  System.out.println("Bird sing.....");
 }
 没有构成方法覆盖,但是构成方法重载。

多态

1、向上转型和向下转型的概念。

  • 向上转型:子--->父 (upcasting)

又被称为自动类型转换:Animal a = new Cat();

  • 向下转型:父--->子 (downcasting)

又被称为强制类型转换:Cat c = (Cat)a; 需要添加强制类型转换符。

 

  • 什么时候需要向下转型?

需要调用或者执行子类对象中特有的方法。

必须进行向下转型,才可以调用。

eg.

 // 分析这个程序能否编译和运行呢?
 // 分析程序一定要分析编译阶段的静态绑定和运行阶段的动态绑定。
 // 只有编译通过的代码才能运行。没有编译,根本轮不到运行。
 // 错误: 找不到符号
 // why??? 因为编译器只知道a5的类型是Animal,去Animal.class文件中找catchMouse()方法
 // 结果没有找到,所以静态绑定失败,编译报错。无法运行。(语法不合法。)
 //a5.catchMouse();
 
 // 假设代码写到了这里,我非要调用catchMouse()方法怎么办?
 // 这个时候就必须使用“向下转型”了。(强制类型转换)
 // 以下这行代码为啥没报错????
 // 因为a5是Animal类型,转成Cat,Animal和Cat之间存在继承关系。所以没报错。
 Cat x = (Cat)a5;
 x.catchMouse(); //猫正在抓老鼠!!!!
  • 向下转型有风险吗?

容易出现ClassCastException(类型转换异常)

eg.

 // 向下转型有风险吗?
 Animal a6 = new Bird(); //表面上a6是一个Animal,运行的时候实际上是一只鸟儿。
 /*
    分析以下程序,编译报错还是运行报错???
    Cat y = (Cat)a6;
    y.catchMouse();
     
    编译器检测到a6这个引用是Animal类型,
    而Animal和Cat之间存在继承关系,所以可以向下转型。
    编译没毛病。
 
    运行阶段,堆内存实际创建的对象是:Bird对象。
    在实际运行过程中,拿着Bird对象转换成Cat对象
    就不行了。因为Bird和Cat之间没有继承关系。
 
    运行是出现异常,这个异常和空指针异常一样非常重要,也非常经典:
    java.lang.ClassCastException:类型转换异常。
    java.lang.NullPointerException:空指针异常。这个也非常重要。
 */

 

  • 怎么避免这个风险?

instanceof运算符,可以在程序运行阶段动态的判断某个引用指向的对象是否为某一种类型。

养成好习惯,向下转型之前一定要使用instanceof运算符进行判断。

eg.

 // 怎么避免ClassCastException异常的发生???
    /*    
    新的内容,运算符:
    instanceof (运行阶段动态判断)
    第一:instanceof可以在运行阶段动态判断引用指向的对象的类型。
    第二:instanceof的语法:
    (引用 instanceof 类型)
    第三:instanceof运算符的运算结果只能是:true/false
    第四:c是一个引用,c变量保存了内存地址,指向了堆中的对象。
        假设(c instanceof Cat)为true表示:
        c引用指向的堆内存中的java对象是一个Cat。
        假设(c instanceof Cat)为false表示:
        c引用指向的堆内存中的java对象不是一个Cat。
 
    程序员要养成一个好习惯:
    任何时候,任何地点,对类型进行向下转型时,一定要使用
    instanceof 运算符进行判断。(java规范中要求的。)
    这样可以很好的避免:ClassCastException
    */
  System.out.println(a6 instanceof Cat); //false
 
  if(a6 instanceof Cat){ // 如果a6是一只Cat
        Cat y = (Cat)a6; // 再进行强制类型转换
        y.catchMouse();
  }
  • 不管是向上转型还是向下转型,首先他们之间必须有继承关系,这样编译器就不会报错。

注意:以后在工作过程中,和别人聊天时要专业一些,说向上转型和向下转型,不要说自动类型转换,也不要说强制类型转换,因为自动类型转换和强制类型转换是使用在基本数据类型方面的,在引用类型转换这里只有向上转型和向下转型。

2、什么是多态?

多种形态,多种状态,编译和运行有两个不同的状态。

1.编译期叫做 静态绑定

2.运行期叫做 动态绑定

eg.分析以下代码:

 Animal a = new Cat();
 a.move();

先来分析编译阶段:

编译的时候编译器发现a的类型是Animal,所以编译器在检查语法的时候,会去Animal.class字节码文件中找move()方法,找到了,绑定上move()方法,编译通过。(静态绑定)

再来分析运行阶段:

但是运行的时候,和底层堆内存当中的实际对象有关,真正执行的时候会自动调用“堆内存中真实对象”的相关方法。(动态绑定)

  • 多态的典型代码:父类型的引用指向子类型的对象。

eg.

 public class Test01{
  public static void main(String[] args){
  Animal a1 = new Animal();
  a1.move(); //动物在移动!!!
  Cat c1 = new Cat();
  c1.move(); //cat走猫步!
  Bird b1 = new Bird();
  b1.move(); //鸟儿在飞翔!!!
 
        // 代码可以这样写吗?
        /*
            1、Animal和Cat之间有继承关系吗?有的。
            2、Animal是父类,Cat是子类。
            3、Cat is a Animal,这句话能不能说通?能。
            4、经过测试得知java中支持这样的一个语法:
            父类型的引用允许指向子类型的对象。
            Animal a2 = new Cat();
            a2就是父类型的引用。
            new Cat()是一个子类型的对象。
            允许a2这个父类型引用指向子类型的对象。
        */
        Animal a2 = new Cat();
        Animal a3 = new Bird();
         
  // 没有继承关系的两个类型之间存在转型吗?
  // 错误: 不兼容的类型: Dog无法转换为Animal
  // Animal a4 = new Dog();
  // 调用a2的move()方法
 
        /*
            什么是多态?
            多种形态,多种状态。
            分析:a2.move();
            java程序分为编译阶段和运行阶段。
            先来分析编译阶段:
            对于编译器来说,编译器只知道a2的类型是Animal,
            所以编译器在检查语法的时候,会去Animal.class
            字节码文件中找move()方法,找到了,绑定上move()
            方法,编译通过,静态绑定成功。(编译阶段属于静态绑定。)
 
            再来分析运行阶段:
            运行阶段的时候,实际上在堆内存中创建的java对象是
            Cat对象,所以move的时候,真正参与move的对象是一只猫,
            所以运行阶段会动态执行Cat对象的move()方法。这个过程
            属于运行阶段绑定。(运行阶段绑定属于动态绑定。)
 
            多态表示多种形态:
            编译的时候一种形态。
            运行的时候另一种形态。
        */
  a2.move(); //cat走猫步!
 
        // 调用a3的move()方法
        a3.move(); //鸟儿在飞翔!!!
 // ======================================================================
  Animal a5 = new Cat(); // 底层对象是一只猫。
        // 分析这个程序能否编译和运行呢?
        // 分析程序一定要分析编译阶段的静态绑定和运行阶段的动态绑定。
        // 只有编译通过的代码才能运行。没有编译,根本轮不到运行。
        // 错误: 找不到符号
        // why??? 因为编译器只知道a5的类型是Animal,去Animal.class文件中找catchMouse()方法
        // 结果没有找到,所以静态绑定失败,编译报错。无法运行。(语法不合法。)
        //a5.catchMouse();
 
        // 假设代码写到了这里,我非要调用catchMouse()方法怎么办?
        // 这个时候就必须使用“向下转型”了。(强制类型转换)
        // 以下这行代码为啥没报错????
        // 因为a5是Animal类型,转成Cat,Animal和Cat之间存在继承关系。所以没报错。
        Cat x = (Cat)a5;
        x.catchMouse(); //猫正在抓老鼠!!!!
 
        // 向下转型有风险吗?
        Animal a6 = new Bird(); //表面上a6是一个Animal,运行的时候实际上是一只鸟儿。
 
        /*
            分析以下程序,编译报错还是运行报错???
            编译器检测到a6这个引用是Animal类型,
            而Animal和Cat之间存在继承关系,所以可以向下转型。
            编译没毛病。
 
            运行阶段,堆内存实际创建的对象是:Bird对象。
            在实际运行过程中,拿着Bird对象转换成Cat对象
            就不行了。因为Bird和Cat之间没有继承关系。
 
            运行是出现异常,这个异常和空指针异常一样非常重要,也非常经典:
            java.lang.ClassCastException:类型转换异常。
            java.lang.NullPointerException:空指针异常。这个也非常重要。
        */
  //Cat y = (Cat)a6;
  //y.catchMouse();
 
  // 怎么避免ClassCastException异常的发生???
        /*    
            新的内容,运算符:
            instanceof (运行阶段动态判断)
            第一:instanceof可以在运行阶段动态判断引用指向的对象的类型。
            第二:instanceof的语法:
            (引用 instanceof 类型)
            第三:instanceof运算符的运算结果只能是:true/false
            第四:c是一个引用,c变量保存了内存地址指向了堆中的对象。
                假设(c instanceof Cat)为true表示:
                c引用指向的堆内存中的java对象是一个Cat。
                假设(c instanceof Cat)为false表示:
                c引用指向的堆内存中的java对象不是一个Cat。
 
            程序员要养成一个好习惯:
            任何时候,任何地点,对类型进行向下转型时,一定要使用
            instanceof 运算符进行判断。(java规范中要求的。)
            这样可以很好的避免:ClassCastException
        */
 
        System.out.println(a6 instanceof Cat); //false
        if(a6 instanceof Cat){ // 如果a6是一只Cat
            Cat y = (Cat)a6; // 再进行强制类型转换
            y.catchMouse();
        }
  }
 }

3、多态在开发中有什么作用?

非常重要:五颗星。。。。(多态你会天天用,到处用!!!!)

 

多态在开发中的作用是:

降低程序的耦合度,提高程序的扩展力。

 public class Master{
    public void feed(Dog d){}
    public void feed(Cat c){}
 }

以上的代码中表示:Master和Dog以及Cat的关系很紧密(耦合度高)。导致扩展力很差。

 

 public class Master{
    public void feed(Pet pet){
        pet.eat();
  }
 }

以上的代表中表示:Master和Dog以及Cat的关系就脱离了,Master关注的是Pet类。

这样Master和Dog以及Cat的耦合度就降低了,提高了软件的扩展性。

 

面向对象的三大特征:

封装、继承、多态

真的是一环扣一环。

 

有了封装,有了这种整体的概念之后。

对象和对象之间产生了继承。

有了继承之后,才有了方法的覆盖和多态。

 

这里提到了一个软件开发原则:

七大原则最基本的原则:OCP(对扩展开放,对修改关闭)

目的是:降低程序耦合度,提高程序扩展力。

面向抽象编程,不建议面向具体编程。

 

4、解释之前遗留的问题

  • 私有方法无法覆盖。

  • 方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。(这是因为方法覆盖通常和多态联合起来)

1、方法覆盖需要和多态机制联合起来使用才有意义。

 Animal a = new Cat();
 a.move();
  • 要的是什么效果?

编译的时候move()方法是Animal的。

运行的时候自动调用到子类重写move()方法上。

 

  • 假设没有多态机制,只有方法覆盖机制,你觉得有意义吗?

没有多态机制的话,方法覆盖可有可无。

没有多态机制,方法覆盖也可以没有,如果父类的方法无法满足

子类业务需求的时候,子类完全可以定义一个全新的方法。

 

  • 方法覆盖和多态不能分开。

 

2、静态方法存在方法覆盖吗?

多态自然就和对象有关系了。

而静态方法的执行不需要对象。

所以,一般情况下,我们会说静态方法“不存在”方法覆盖。

不探讨静态方法的覆盖。

eg.

 public class OverrideTest05{
  public static void** main(String[] args){
        // 静态方法可以使用“引用.”来调用吗?可以
        // 虽然使用“引用.”来调用,但是和对象无关。
        Animal a = **new** Cat(); //多态
        // 静态方法和对象无关。
        // 虽然使用“引用.”来调用。但是实际运行的时候还是:Animal.doSome()
        a.doSome();    
        Animal.doSome();
        Cat.doSome();
  }
 }
 
 class Animal{
    // 父类的静态方法
    public static void doSome(){
        System.out.println("Animal的doSome方法执行!");
    }
 }
 
 class Cat extends Animal{
    // 尝试在子类当中对父类的静态方法进行重写
  public static void doSome(){
  System.out.println("Cat的doSome方法执行!");
  }
 }

总结两句话:

私有不能覆盖。

静态不谈覆盖。

 

  • 学习了多态机制之后:

在方法覆盖中,关于方法的返回值类型。

什么条件满足之后,会构成方法的覆盖呢?

1、发生具有继承关系的两个类之间。

2、父类中的方法和子类重写之后的方法:

具有相同的方法名、相同的形式参数列表、相同的返回值类型

 

“相同的返回值类型”可以修改一下吗?

对于返回值类型是基本数据类型来说,必须一致。

对于返回值类型是引用数据类型来说,重写之后返回值类型可以变的更小(但意义不大,实际开发中没人这样写。)。

 

推荐阅读