重载和重写都是面向对象编程中的概念,但我们或许还听说过一种叫做覆写(overwrite)的概念。C++ 是拥有这个概念的,Java 只有 overload 和 override,Python 只有隐式的 overload 和 override,没有 overwrite 的概念。在重载(overload)、重写(override)和覆写(overwrite)中,我们一般对前面两个比较熟悉,对最后一个会略微陌生一些。
重载(overload)
重载的定义及规则
重载是指在一个类中,可以定义多个方法名相同但参数类型、个数或顺序不同的方法。JVM 编译器会根据传入的参数自动匹配并调用对应的方法。
重载的目的是为了让程序员能够使用同一个方法名来处理多种不同类型的参数。通过方法重载,程序员可以根据实际需要创建多个具有相同名称但参数列表不同的方法,这些方法可以用于执行类似但不完全相同的操作。这样做可以使代码更加简洁、灵活和易于维护,同时也提高了代码的复用性。最典型的,System.out.println() 方法就是一个被重载过的方法,它可以接受各种不同类型的参数,并且输出它们对应的字符串表示形式。
重载必须满足下面的规则:
- 参数列表必须改变;
- 返回类型可以改变;
- 实现过程可以改变;
- 异常声明可以改变;
- 访问限制可以改变;
总结:外壳必须改变,内核可以改变。
重载方法的参数列表不能相同是因为编译器需要根据调用时传递的实际参数类型来确定要调用哪个重载方法。如果两个重载方法的参数列表相同,则编译器无法区分它们,并且会导致编译错误。至于返回类型和实现过程等,这些部分并不会影响到 JVM 对重载方法的区分,自然也就没有任何要求,是否修改都是可以的。
下面是一个简单的重载示例:
public class Overload {
public static void print(Integer a) {
// 当传入参数的类型为Integer时,JVM会调用这个方法
System.out.println("传入参数为Integer类型");
}
public static void print(String s) {
// 当传入参数的类型为String时,JVM会调用这个方法
System.out.println("传入参数为String类型");
}
public static void main(String[] args) {
Overload.print(1); // Output:传入参数为Integer类型
Overload.print("1"); // Output:传入参数为String类型
}
}
但其实重载中 JVM 自动寻找并匹配参数列表的过程,我们可以在一定程度上手动模拟完成:
public class Overload {
public static void print(Object o) {
// 模拟 JVM 匹配参数列表的过程
if (o instanceof Integer)
System.out.println("传入参数为Integer类型");
else if (o instanceof String)
System.out.println("传入参数为String类型");
else
throw new Error("Unresolved compilation problem:");
}
public static void main(String[] args) {
Overload.print(1); // Output:传入参数为Integer类型
Overload.print("1"); // Output:传入参数为String类型
}
}
其他编程语言中的重载
C++ 和 Java 在类的方法重载上面极其相似,不再详述。
Python 的重载就不太一样了,它是一门动态类型的语言,变量和参数没有确定的类型,自然没有真正的重载了。它的重载类似于上面手动模拟重载的过程,算是隐式的重载。在 Python 的内置库 typing 中有一个 overload 装饰器可以让 IDE 理解这是一个重载函数或者方法,但这并不是真正的重载,而是虚拟的。
from typing import overload
@overload
def fake_overload(x: int) -> None: ... # 没有实现过程,实际被覆盖
@overload
def fake_overload(x: str) -> None: ... # 没有实现过程,实际被覆盖
def fake_overload(x: int | str) -> None: # 真正实现过程的部分
""" 伪重载 """
if type(x) == int:
print('传入参数为int类型')
elif type(x) == str:
print('传入参数为str类型')
else:
raise TypeError
重写(override)
重写的定义及规则
重写是指子类重新实现父类中已有的方法,此方法可使用 @Override 注解来标记(非强制)。子类的方法必须与父类被重写的方法具有相同的名称、返回类型和参数列表。
重写,顾名思义就是重新编写原来的代码。Java 中重写的目的是让子类能够重新定义父类中已有的方法,从而实现多态性。通过重写,子类可以根据自己的需求来实现继承自父类的方法,使得代码更加灵活和可复用。同时,重写也可以提高程序的可读性和维护性。但是要注意一点,重写并不代表子类再也无法调用父类中被重写的方法了,子类仍可以通过 super 关键字进行调用。
重写必须满足以下规则:
- 参数列表不能改变;
- 返回类型可以为被重写方法的派生类(java5及之前版本完全不能改变);
- 实现过程可以改变;
- 异常声明不能比父类更加宽泛;
- 访问限制不能比父类更加严格;
- final 修饰的方法不可重写!
- static 修饰的方法不可重写,但能重新声明!
- 构造方法不可重写!
- 父类无法被子类访问的方法不可重写!
总结:外壳(几乎)不能改变,内核可以改变。
重写可以说是父类方法的特化,所以从代码的具体程度上来说,子类重写后方法的具体程度比父类的更加大,自然而然地,重写后的方法抛出异常的声明不能比父类的更加宽泛。当然,父类中有些方法子类无法访问到,那么也就不存在所谓的重写。
特别说明一下,static 修饰的父类方法是不可重写的,子类照着父类的去”重写“也并非真正的重写,尽管程序可以运行。子类实际上只是重新声明,定义了一个新的、独立于父类的静态方法,对于父类的那个同名方法,只是相当于把它”隐藏“起来了而已,并没有真的重写。父类无法被子类访问的方法也是类似的道理,都是子类重新定义了一个新的方法罢了。
下面是一个简单的重写示例:
class Animal {
public static void move() { // static 修饰,不可被重写
System.out.println("动物移动");
}
public void bark() { // 被重写方法
System.out.println("动物叫");
}
}
class Dog extends Animal {
// @Override // 错误的重写,添加注解则编译器报错
public static void move() { // 重新声明,并非重写
System.out.println("狗跑");
}
@Override // 重写注解
public void bark() { // 重写方法
System.out.println("狗吠");
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Dog dog = new Dog();
Animal.move(); // Output:动物移动
Dog.move(); // Output:狗跑
animal.bark(); // Output: 动物叫
dog.bark(); // Output: 狗吠
}
}
@Override 注解
@Override 是一种注解(Annotation),它用于标记一个方法是重写了父类或接口了的同名方法。使用 @Override 注解可以让编译器检查该方法是否正确地重写了父类中的方法。如果没有正确重写,则编译器会提示错误。
注解 @Override 通常会用于下面两种情况:
- 子类重写父类方法时使用,以保证正确地重写,若错误重写(不满足规则)则编译报错;
- 重写接口中的抽象方法时使用, 以确保实现了接口中的所有抽象方法;
虽然注解 @Override 并非强制使用的,但加上它可以提高代码的可读性和可维护性,我们应该养成使用 @Override 的好习惯。
其他编程语言中的重写
在 Python 里面的重写非常直白,没有任何特殊的要求,只要子类与父类的方法同名,就可以重写。而 C++ 里的重写和 Java 也比较相似。
区别与联系
下面是一张总结性的表格:
方式 | 重载(overload) | 重写(override) | 覆写(overwrite) |
参数列表 | 必须改变 | 不能改变 | \ |
返回类型 | 可以改变 | (几乎)不能改变 | \ |
实现过程 | 可以改变 | 可以改变 | \ |
异常声明 | 可以改变 | 不能比父类更加宽泛 | \ |
访问限制 | 可以改变 | 不能比父类更加严格 | \ |
备注 | final 关键字修饰的方法不可重写 子类无法访问的父类方法不可重写 static 关键字修饰的方法不可重写,但可重新声明 构造方法不可重写 | \ | |
总结 | 外壳必须改变,内核可以改变 | 外壳(几乎)不能改变,内核可以改变 | \ |
下面一张图片生动地展示了什么是重载和重写:
这里要特别强调的是,Java 中的构造方法只能被重载而不能被重写:
子类构造方法可以与父类构造方法同名,但参数列表必须不同,这就是方法重载的特性。子类构造方法可以调用父类构造方法来初始化从父类继承下来的属性或行为,即使用 super 关键字来调用父类的构造方法。重写是指子类重写了父类中已有的方法,并且方法名、参数列表和返回值类型都相同,在调用该方法时会优先调用子类中的方法而非父类中的方法。