面试题: 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分

面试官考察点猜想

这道题纯粹只是考查基础理论知识,对实际开发工作中没有太多的指导意义,毕竟编辑器都有语法提示功能,如果没写正确,会有错误提示。

背景知识详解

关于重载(Overload)和重写(Override),在实际开发中使用非常频繁,涉及到的背景知识并不难。

重写

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

重写是发生在类的继承关系,或者类的实现关系中的,重写后的方法和原方法需要保持完全相同的返回值类型、方法名、参数个数以及参数类型,简单来说,就是子类重写的方法必须和父类保持完全一致

类的继承关系

我们来看下面这个基于继承关系的例子。

class Animal{
public void move(){
System.out.println("动物可以移动");
}
}

class Bird extends Animal{
public void move(){
System.out.println("鸟可以飞");
}
}
class Dog extends Animal{
public void move(){
System.out.println("狗可以跑")
}
}

public class TestMain{
public static void main(String args[]){
Animal a = new Animal(); // Animal 对象
Animal b = new Bird(); //Bird对象
Animal c = new Dog(); // Dog 对象

a.move();// 执行 Animal 类的方法
b.move(); //执行Bird类的方法
c.move();//执行 Dog 类的方法
}
}

上述程序运行的结果

动物可以移动
鸟可以飞
狗可以跑

在这个案例中,Animal是一个属于动物的抽象类,它定义了一个方法move()。表示动物的具有的行为。

而动物只是一个泛类别,具体到某种动物时,行为方式是不同的,因此定义了BirdDoc,分别继承了Animal这个类,并且重写了move()方法,分别实现这两种动物的行为方式。

重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

在类继承关系中,父类的非抽象方法,子类是不强制要求重写的。在实际应用中,如果重写了父类的方法,并且实例对象的引用指向的是子类时,JVM会自动调用子类重写的方法,此时,父类的方法完全被屏蔽了。就像前面测试的代码。

父类引用指向子类实现Dog(),此时调用c.move()方法,只会调用到Dog类中的move()方法。如果Dog子类没有重写move()方法,则会调用父类Animalmove()方法。

Animal c = new Dog(); // Dog 对象

c.move();//执行 Dog 类的方法

在有些情况下,子类重写了父类的方法,我们希望在调用子类重写方法的同时,仍然能够调用到父类被重写的方法,怎么实现?

Super关键字

当需要在子类中调用父类的被重写方法时,要使用 super 关键字。

class Animal{
public void move(){
System.out.println("动物可以移动");
}
}

class Bird extends Animal{
public void move(){
super.move(); //增加super调用
System.out.println("鸟可以飞");
}
}

}

public class TestMain{
public static void main(String args[]){
Animal b = new Bird(); //Bird对象
b.move();//执行 Bird 类的方法
}
}


运行结果如下:

动物可以移动
鸟可以飞

方法的重写规则

总结一下,在Java中,方法重写的规则。

  • 参数列表与被重写方法的参数列表必须完全相同。
  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
  • 父类的成员方法只能被它的子类重写。
  • 声明为 final 的方法不能被重写。
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个类,则不能重写该类的方法。

基于接口实现的重写

基于接口实现的重写,在实际应用中,使用非常频繁,以线程实现为例,如图所示,表示Thread和Runnable的类关系图。

image-20211027213311085

Runnable是一个接口,它定义了线程的执行方法,代码如下:

@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}

在实际应用中,我们可以直接继承这个接口来声明一个线程。

Thread,是一个普通的线程类,它实现了Runnable接口,并且重写了Runnable这个接口的run方法,这里这么设计的目的是: 避免Java中一个类只能实现一个接口这一规则导致,如果一个类已经继承了其他的接口,但是又想要去实现线程时的问题。

public
class Thread implements Runnable {
@Override
public void run() {
if (target != null) {
target.run();
}
}
}

由于接口只是用来做规范设计,用来描述某个对象具有什么行为,但是它并没有具体的实现,因此如果需要声明一个线程,就需要实现该接口并且重写里面的抽象方法(接口中未实现的方法都是抽象的,子类必须要重写)。

Thread类中重写了Runnable中的run方法,该方法调用了target.run()。这个target是真正的线程业务实现,Thread只是一个委派设计模式。

因此,如果我们想通过继承Thread来实现线程,则需要按照如下代码的写法来实现,其中target就是代表着子类的App这个对象实例。

public class App extends Thread{
@Override
public void run() {
//doSomething
}
}

由于接口只是一种行为规范,本身不提供实现,因此实现接口的子类,都“必须”要重写父类的方法,这个和类继承是有区别的。

重载

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

最常用的地方就是构造器的重载,比如在ThreadPoolExecutor线程池的实现类中,可看到如下的重载方法。

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}

方法重载的好处就是让类以统一的方式处理不同类型的一种手段,调用方法时通过传递给他们的不同个数和类型的参数来决定具体使用哪个方法,这就是多态性。

它的特点是:重载发生在本类,方法名相同,参数列表不同,与返回值无关,只和方法名,参数的类型相关。

方法重载时,方法之间需要存在一定的联系,因为这样可以提高程序的可读性,并且我们一般只重载功能相似的方法。

重载规则

  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。

问题解答

理解了上述知识点以后,再来看这道面试题。

面试题: 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分

区别:

  1. 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  2. 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  3. 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

重载方法是否能够根据返回类型进行区分

重载方法无法根据类型来区分, 它只能通过参数类型、参数个数来区分,但是对于重载的方法,是允许修改返回值类型、异常类型、访问等级,但是不能只根据这些类型类做重载。

为什们不能仅根据返回类型来区分重载呢?

原因是,在调用目标方法时,是无法指定返回值类型信息的,这个时候编译器并不知道你要调用哪个函数。

比如在下面这段代码中,当调用max(1,2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。

float max(int a, int b);
int max(int a, int b);

可能有同学会问,如果让编译器能够根据上下文语境来判断呢?比如像下面这段代码。

float x=max(1,2);
int y=max(2,3);

在实际开发中,很多时候会存在这样一种方法调用max(1,2),并不会去声明返回值,由于这种情况的存在,所以这个理论也不能实现。

函数的返回值只是作为函数运行之后的一个“状态”他是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”

问题总结

这个问题,其实是属于那种,你不问我,我一定会认为自己知道,而且在工作开发中也能使用不会出问题,但是你一问我,我一定会懵逼,不是因为真的不懂,而是不知道怎么去组织语言来描述这两个概念。

建议大家参考“费曼学习法”,就是把这篇文章学到的理论,通过演讲的方式表达出来,可以和同事,或者自己自问自答。

upload successful