【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