【OOP】重写(Overriding)与重载(Overloading)

Posted by 西维蜀黍 on 2019-04-01, Last Modified on 2021-09-21

背景

重载(Overloading)和重写(Overriding)是面向对象程序设计(Object-oriented programming)中两个比较重要的概念。但是对于新手来说也比较容易混淆。

定义

重载(Overload)

简单说,**重载(Overload)**就是函数或者方法有同样的名称,但是参数列表(方法的签名)不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。

在编译阶段,虚拟机会根据参数的静态类型决定使用哪个重载版本(方法在实际运行时内存中的入口地址),即静态分派

重写(Override)

**重写(Override)**指的是在Java的子类与父类中有两个名称、参数列表都相同(这也称为方法的签名相同)的方法的情况。由于他们具有相同的方法签名,所以子类中的新方法将覆盖父类中原有的方法。

我们用的比较多常用@Override(覆盖标记),使用 @Override 标记可以让编译器帮助检查,在子类中被标识为@Override的方法的签名,是否与父类中的同名方法一致(如果不一致,则意味着在父类中,不存在被重写的对应方法)。

当子类重新定义了父类的方法实现后,在运行期根据引用变量的类型确定方法的执行版本(方法在实际运行时内存中的入口地址),即动态分派

重载 VS 重写

关于重载和重写,你应该知道以下几点:

  • 重载是一个编译期概念,重写是一个运行期间概念。
  • 重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法。
  • 重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法
  • 因为在编译期已经确定调用哪个方法,所以重载并不是多态。而重写是多态。重载只是一种语言特性,是一种语法规则,与多态无关,与面向对象也无关(注:严格来说,重载是编译时多态,即静态多态。但是,Java中提到的多态,在不特别说明的情况下都指动态多态)。

重写(Override)说明

重写的条件

  • 参数列表必须完全与被重写方法的相同;
  • 返回类型必须完全与被重写方法的返回类型相同;
  • 访问级别的限制性一定不能比被重写方法的宽;
  • 访问级别的限制性可以比被重写方法的窄;
  • 重写方法一定不能抛出新的检查异常,或比被重写的方法声明的检查异常更广泛的检查异常;
  • 重写的方法能够抛出更少或更有限的异常(也就是说,被重写的方法声明了异常,但重写的方法可以什么也不声明);
  • 不能重写被声明为final的方法;
  • 如果不能继承一个方法,则不能重写这个方法。

@Override(覆盖标记)

我们用的比较多常用@Override(覆盖标记),使用@Override标记可以让编译器帮助检查,在子类中被标识为@Override的方法的签名,是否与父类中的同名方法一致。

我们强烈建议,如果我们打算在子类中覆盖一个父类中的方法,一定要在子类的这个对应方法上打上@Override标记。

例子1

在重写父类的onCreate时,在方法前面加上@Override ,编译器可以帮你检查子类中这个方法的声明正确性。

@Override
public void onCreate(Bundle savedInstanceState){
	……
}

比如,以上的写法是正确的,如果你写成:

@Override
public void oncreate(Bundle savedInstanceState){
	……
}

编译器会报如下错误:The method oncreate(Bundle) of type HelloWorld must override or implement a supertype method,以确保你正确重写onCreate方法(因为oncreate应该为onCreate)。而如果你不加@Override,则编译器将不会检测出错误,而是会认为你为子类定义了一个新方法:oncreate。

重写的例子

下面是一个重写的例子,看完代码之后不妨猜测一下输出结果:

class Dog{
    public void bark(){
        System.out.println("woof ");
    }
}
class Hound extends Dog{
    public void sniff(){
        System.out.println("sniff ");
    }
		@Override
    public void bark(){
        System.out.println("bowl");
    }
}

public class OverridingTest{
    public static void main(String [] args){
        Dog dog = new Hound();
        dog.bark();
    }
}

输出结果

bowl

分析

上面的例子中,dog 变量被声明为Dog类型。

在编译期,编译器会检查Dog类中是否有可访问的bark()方法,只要其中包含bark()方法,那么就可以编译通过。

在运行期,Hound对象被new出来,并赋值给dog变量,这时,JVM是明确地知道dog变量指向的其实是一个Hound对象。所以,当dog调用bark()方法的时候,就会调用Hound类中定义的bark()方法。这就是所谓的动态多态性


而事实上,如果在Hound类中,我们去掉 public void bark() 方法声明上的@Override,上面例子的运行结果同样也会是 bowl。这说明加不加@Override并不会影响影响结果,但是加上@Override可以让编译器帮我们进行编译检查,以确保重写方法的声明没有问题。

因此,再次强调,如果我们打算在子类中覆盖一个父类中的方法,一定要在子类的这个对应方法上打上@Override标记。

重载(Overload)的说明

重载的例子

class Dog{
    public void bark(){
        System.out.println("woof ");
    }

    //overloading method
    public void bark(int num){
        for(int i=0; i<num; i++)
            System.out.println("woof ");
    }
}

上面的代码中,定义了两个bark方法,一个是没有参数的bark方法,另外一个是包含一个int类型参数的bark方法。

在编译期,编译期可以根据方法签名(方法名和参数情况)情况确定哪个方法被调用。

重载的条件

  • 被重载的方法必须改变参数列表;
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。

常见的面试笔试题

1 下面这段代码的输出结果是什么?

public class Test {
    public static void main(String[] args) {
        Shape shape = new Circle();
        System.out.println(shape.name);
        shape.printType();
        shape.printName();
    }
}

class Shape {
    public String name = "shape";

    public Shape() {
        System.out.println("shape constructor");
    }

    public void printType() {
        System.out.println("printType shape");
    }

    public static void printName() {
        System.out.println("printName shape");
    }
}

class Circle extends Shape {
    public String name = "circle";

    public Circle() {
        System.out.println("circle constructor");
    }

    public void printType() {
        System.out.println("printType circle");
    }

    public static void printName() {
        System.out.println("printName circle");
    }
}

输出

shape constructor
circle constructor
shape
printType circle
printName shape

分析

这道题主要考察了隐藏和覆盖的区别。

覆盖只针对非静态方法,而隐藏是针对成员变量和静态方法的。

这2者之间的区别是:覆盖受RTTI(Runtime type identification)约束的,而隐藏却不受该约束。也就是说只有覆盖方法才会进行动态绑定,而隐藏是不会发生动态绑定的。

在Java中,除了static方法和final方法,其他所有的方法都是动态绑定。因此,就会出现上面的输出结果。

Reference