码出高效读书笔记:Override和Overload

1、Override(覆写)

如果父类定义的方法达不到子类的期望,那么子类可以重新实现方法覆盖父类的实现。因为有些子类是延迟加载的,甚至是网络加载的,所以最终的实现需要在运行期判断,这就是所谓的动态绑定。

动态绑定是多态性得以实现的重要因素,元空间有一个方法表保存着每个可以实例化类的方法信息,JVM可以通过方法表快速地激活实例方法。如果某个类覆写了父类的某个方法,则方法表中的方法指向引用会指向子类的实现处。代码通常是用这样的方式来调用子类的方法,通常这也被称为向上转型。如下例:

//父类引用指向子类对象
Father father = new Son();
//Son覆写了此方法
father.doSomething();

//Son s = (Son)father是向下转型

PS:自己的一段感想

之前我一直不太明白向上转型的用处,如果你想调用子类的方法,你用子类类型的引用指向子类对象不也是一样的吗?为什么还要使用向上转型?但是最近我终于明白了,使用向上转型可以有效的减少代码量,先看下面这一段我写的代码

package Test;

import java.util.Scanner;
//这是父类交通工具
public class Transportation {
    public void yunfei(){
        System.out.println("这是父类方法");
    }

    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        //这个for循环我是为了多测试几次,可以忽略
        for(int i=0;i<3;i++) {
            System.out.println("请输入您选择的交通工具:"); 
            Transportation T = TransportationFactory.select(scan.next());
            T.yunfei();
        }
    }
}

//子类:卡车类
class truck extends Transportation{
    @Override
    public void yunfei(){
        System.out.println("这是卡车运货");
    }
}

//子类:火车类
class train extends Transportation{
    @Override
    public void yunfei(){
        System.out.println("这是火车运货");
    }
}

//子类:飞机类
class plane extends Transportation{
    @Override
    public void yunfei(){
        System.out.println("这是飞机运货");
    }
}

//子类:其他交通工具类(其实这个类不需要,可以使用别的方法来处理,但是为了更加明白的演示,我就先这么写了
class other extends Transportation{
    @Override
    public void yunfei(){
        System.out.println("对不起,我们暂时不提供这种运输工具");
    }
}

//这是一个工厂类,用来生产交通工具对象
class TransportationFactory{
    public static Transportation select(String which){
        if (which.equals("truck")){
            return new truck();
        }else if (which.equals("train")){
            return new train();
        }else if (which.equals("plane")){
            return new plane();
        }else {
            return new other();
        }
    }
}

这是运行结果:

如上代码所示,如果不使用向上转型的话,我们用子类引用指向子类对象的话,还要多写Truck truck = new Truck()等等代码,有多少个子类就要多写多少行,上面这段代码代码量很少,可以那么写,但是如果是一个大工程的话,不使用向上转型就会让代码体积更庞大,不利于处理,使用向上转型的话,就只要写一行代码,用父类类型的引用指向工厂类生产出来的子类对象就OK了。

向上转型关心的是被子类覆写的方法,它不关心子类新拥有的方法,目的就是为了实现多态,我对多态的理解就是一个对象的多种形态,大家只有在会同一个动作的前提下,才能把这个动作做出不一样的花样来,比如说“走路”这个动作,有的人走得快,有的人走得慢,但是前提是大家都要会走路这个动作,如果你连这个大家都能做的动作都不会的话,那你说你也能把这个动作玩出花样来,那不是扯淡吗。


向上转型时,通过父类引用执行子类方法时需要注意以下两点:

  1. 无法调用到子类中存在而父类中不存在的方法。
  2. 可以调用到子类中覆写了父类的方法,这是一种多态实现。

覆写父类方法要满足的4个条件:

  1. 访问权限不能更小。访问控制权限变小意味着在调用时父类的可见方法无法被子类多态执行,比如父类中方法时用public修饰的,子类覆写时编程private。摄像如果编译器为多态开启了后门,让在父类定义中可见的方法随着父类调用链路下来,执行了子类更小权限的方法,则破坏了封装。如下列代码所示,在实际编码中不允许将方法访问权限缩小:
    class Father{
        public void method(){
            System.out.println("Father's method");
        }
    }
    
    class Son extends Father{
        //编译报错,不允许修改为访问权限更严格的修饰符
        @override
        private void method(){
            System.out.println("Son's method");
        }
    }
  2. 返回类型能够向上转型成为父类的返回类型(基本上子类覆写的方法的返回值类型要与父类被覆写的方法返回值类型相同)。虽然方法的返回值不是方法签名(方法名+参数列表)的一部分,但是在覆写时,父类的方法表指向了子类实现方法,编译器会检查返回值是否向上兼容。注意,这里的向上转型必须是严格的继承关系,数据类型基本不存在通过集成向上转型的问题。比如int和Integer是非兼容返回类型,不会自动装箱。再比如,如果子类方法返回int,而父类方法返回long,虽然数据表示范围更大,但是它们之间没有继承关系。返回类型是Object的方法,能够兼容任何对象,包括class、enum、interface等类型。

  3. 异常也要能向上转型成为父类的异常(子类覆写的方法抛出的异常不能大于父类被覆写的方法抛出的异常)。异常分为checked(检查型异常)和unchecked(非检查型异常)两种类型。如果父类抛出一个checked异常,则子类只能抛出此异常或此异常的子类。而unchecked异常不用显式地向上抛出,所以没有任何兼容问题。

  4. 方法名、参数类型及个数必须严格一致。为了使编译器准确地判断是否是覆写行为,所有的覆写方法必须加@Override注解。此时编译器会自动检查覆写方法签名(方法名+参数列表)是否一致,避免了覆写时因写错方法名或方法参数而导致覆写失败。此外,@Override注解还可以避免因为权限控制可见范围导致的覆写失败,例如下面的代码,Father类和Son类在两个不同的包中,并且method()方法没有权限控制符,默认是同一包内可见的,所以Son中的method()方法会被当做是一个“新方法”,如果加上@Override,编译器则会提示:Method does not override method from its superclass

    package com.wmh.A;
    import com.wmh.B;
    
    public class Father{
        void method(){
            System.out.println("I'm father");
        }
    }
    package com.wmh.B;
    import com.wmh.A.Father;
    
    public class Son extends Father{
        void method(){
            System.out.println("I'm Son");
        }
    }

综上所述,方法的覆写可以总结成容易记忆的口诀:“一大两小两同”。

  • 一大:子类方法的访问权限控制符只能相同或者更大
  • 两小:子类方法的返回值类型和抛出的异常要比父类的方法更小
  • 两同:方法名和参数列表必须完全相同

覆写注意事项:

覆写只能针对非静态、非final、非构造方法。由于静态方法属于类,如果父类和子类存在同名静态方法,那么两者都可以被正常调用。如果方法有final修饰,则表示此方法不可被覆写。

2、Overload(重载)

在同一各类众,如果多个方法有相同的名字、不同的参数,即为重载,比如一个类中可以有多个构造方法。String类中的valueOf是比较著名的重载案例,它有9个方法,可以将输入的基本数据类型、数组、Object等转化成为字符串。

在编译器眼中,方法名称+参数类型+参数个数,组成一个唯一键,称为方法签名。JVM通过这个唯一键决定调用哪种重载的方法。注意!方法的返回值并不是这个组合体的一员,所以在使用重载机制时,不能有两个方法名称完全相同,参数类型和个数也相同,但是返回类型不同的方法。

重载似乎是比较容易理解和掌握的编程技能,有时仅凭肉眼判断就能知道应调用哪种重载方法,特别是如下代码所示的第一种方法和第二种方法。前者是无参的,后者参数是int param,但是后面的三种方法,只是参数类型不同罢了。这时,如果调用methodForOverload(7),那么到底会调用哪个方法呢?

public class overloadmethods{
    //第一种方法:无参
    public void overloadMethod(){
        Sysout.out.println("无参方法");
    }

    //第二种方法:基本数据类型
    public void methodForOverload(int param){
        System.out.println("参数为基本数据类型int的方法");
    }

    //第三种方法:包装数据类型
    public void methodForOverload(Integer param){
        System.out.println("参数为包装类型Integer的方法");
    }

    //第四种方法:可变参数,可以接受0-n个Integer对象
    public void methodForOverload(Integer...param){
        System.out.println("可变参数方法");
    }

    //第五种方法:Object对象
    public void methodForOverload(Object param){
        System.out.println("参数为Object的方法");
    }
}

下面是这五种方法对应的字节签名,看看有何异同:

//V代表Void返回值类型
public overloadMethod()V

//I就是代表int基本数据类型,而非Integer
public methodForOverload(I)V

//L代表输入参数是对象,然后跟着package+类名     (第一处)
public methodForOverload(Ljava/lang/Integer;)V

//varargs表示可变参数     (第二处)
public varargs methodForOverload([Ljava/lang/Integer;]V

//L同样表示对象参数
public methodForOverload(Ljava/lang/Object;)V

JVM在重载方法中,选择合适的目标方法的顺序如下:

  1. 精准匹配
  2. 如果是基本数据类型,自动转换成更大表示范围的基本类型
  3. 通过自动拆箱与装箱
  4. 通过子类向上转型继承路线依次匹配
  5. 通过可变参数匹配

精准匹配优先,这时毫无疑问的。int在和Integer的较量中胜出,因为不需要自动装箱,所以7会调用int参数的方法,如果是new Integer(7)的话,Integer参数的方法胜出。编译器跑出来的结果如下:

父类的公有实例方法与子类的公有实例方法可以存在重载关系,不管继承关系如何复杂,重载在编译时可以根据规则知道调用哪种目标方法。所以,重载又称为静态绑定

猜你喜欢

转载自blog.csdn.net/weixin_41047704/article/details/85785630
今日推荐