我们来把重载(Overloading)和重写(Overriding)这两个面向对象编程的核心概念掰开揉碎,用大白话和例子讲清楚它们的区别。
一、核心比喻(先记住这个感觉)
重载(Overloading):就像是同一个名字的工具箱,但里面装了功能相似但适用场景不同的工具。比如“扳手工具箱”里可能有:拧小螺丝的小扳手、拧大螺丝的大扳手、拧水管的长柄扳手。它们都叫“扳手”(方法名相同),但根据你要拧的东西不同(参数不同),你选用不同的扳手。
重写(Overriding):就像是儿子继承了老爸的某项手艺,但儿子觉得老爸的做法不够好或者不适合新时代,于是他自己重新定义了一套更好的做法。比如老爸有个“修自行车”的方法(父类方法),儿子继承了,但儿子觉得老爸修得慢,他发明了一套更快的方法。当别人叫“修自行车”时(调用同名同参数的方法),如果对象是儿子,就用儿子的新方法(重写后的方法)。
二、重载(Overloading)
发生在哪里?同一个类里面。
是什么?在同一个类中,允许存在多个同名的方法。
关键要求:这些同名方法的参数列表必须不同。怎么个不同法?
参数个数不同:比如add(int a, int b)和add(int a, int b, int c)
参数类型不同:比如add(int a, int b)和add(double a, double b)
参数顺序不同(通常指类型顺序):比如print(String name, int age)和print(int age, String name)
注意:仅仅返回值类型不同、方法修饰符不同(如static)或者抛出的异常不同,不足以构成重载! 编译器区分重载方法只看“方法签名”,而方法签名 = 方法名 + 参数列表(参数类型、个数、顺序)。
为什么叫“重载”?就是给同一个方法名“装载”了多种不同的实现方式(参数组合),让它能处理更多样的情况。
调用时怎么知道用哪个?在编译时,编译器根据你调用方法时传递的实际参数的类型、个数和顺序,就能精确地确定应该调用哪个重载版本。这叫静态绑定或早绑定。
目的:提高方法的灵活性和可读性。使用者只需要记住一个方法名(比如print或calculate),就能根据不同的需求传入不同的参数,不用记一堆不同名字的方法(如printString, printInt, calculateSum, calculateAverage)。
通俗例子:
你开了一家奶茶店,有个方法叫makeDrink()。
重载1:makeDrink(String teaType) // 做指定类型的纯茶
重载2:makeDrink(String teaType, String milkType) // 做指定茶底+指定奶的奶茶
重载3:makeDrink(String teaType, String milkType, int sugarLevel) // 做指定茶底+指定奶+指定糖度的奶茶
顾客点单时,你说“做杯奶茶”(方法名 makeDrink),店员(编译器)会根据顾客具体说了什么(参数:只要茶?茶+奶?茶+奶+半糖?)来决定执行哪个具体流程。
public class Calculator {
// 重载1:两个整数相加
public int add(int a, int b) {
return a + b;
}
// 重载2:三个整数相加 (参数个数不同)
public int add(int a, int b, int c) {
return a + b + c;
}
// 重载3:两个浮点数相加 (参数类型不同)
public double add(double a, double b) {
return a + b;
}
// 重载4:整数和浮点数相加 (参数类型不同)
public double add(int a, double b) {
return a + b;
}
// 重载5:浮点数和整数相加 (参数顺序不同)
public double add(double a, int b) {
return a + b;
}
// 错误示例:仅返回值不同,不是重载!编译器会报错
// public double add(int a, int b) {
// return (double)(a + b);
// }
}
三、重写(Overriding)
发生在哪里?具有继承关系的父子类之间(子类继承父类)。
是什么?子类觉得从父类继承来的某个方法的实现不适合自己(或者想扩展功能),于是自己重新写一个实现来替代父类的那个方法。
关键要求:
方法名必须相同
参数列表必须完全相同(个数、类型、顺序)
返回值类型:
在Java 5+中,子类方法的返回值类型可以是父类方法返回值类型的子类型(协变返回类型)。
否则,必须和父类方法返回值类型相同。
访问权限:子类方法的访问权限不能比父类方法更严格。例如,父类方法是public,子类重写时不能是protected或private(可以保持 public)。父类方法是protected,子类可以是protected或 public(不能是private)。
异常:子类方法抛出的受检异常(Checked Exception)范围不能比父类方法抛出的更广(可以更少或相同,或者不抛)。
static, final, private方法不能被重写:static方法属于类,与对象无关;final方法禁止修改;private方法在子类不可见。
@Override注解:强烈建议在子类重写方法上加上@Override注解。这会让编译器帮你检查是否真的满足了重写的所有条件(方法签名匹配、访问权限等),避免因拼写错误等原因导致你以为重写了其实没有。
为什么叫“重写”?就是子类把父类的方法实现覆盖掉、重新写了一遍。
调用时怎么知道用哪个?在运行时,JVM根据实际创建的对象类型(而不是引用变量的类型)来决定调用哪个版本的方法。这叫动态绑定或晚绑定。这是实现多态性(Polymorphism)的关键机制。
目的:实现多态。允许子类根据自身需要定制特定的行为,同时对外提供统一的接口(父类定义的方法名)。这是面向对象“一个接口,多种实现”思想的核心体现。
通俗例子:
父类Animal有个方法makeSound() { 输出 "动物叫" }。
子类 Dog 重写了makeSound() { 输出 "汪汪汪!" }。
子类 Cat 重写了makeSound() { 输出 "喵喵喵~" }。
当你写代码:
Animal myAnimal = new Dog(); // 引用是Animal类型,实际对象是Dog
myAnimal.makeSound(); // 输出 "汪汪汪!" (运行时根据实际对象Dog决定)
Animal anotherAnimal = new Cat(); // 引用是Animal类型,实际对象是Cat
anotherAnimal.makeSound(); // 输出 "喵喵喵~" (运行时根据实际对象Cat决定)
虽然myAnimal和anotherAnimal的引用类型都是Animal,但调用makeSound() 时,JVM看它们实际指向的对象是Dog还是Cat,然后调用相应子类重写的方法。这就是多态的魅力!
class Animal {
public void makeSound() {
System.out.println("动物叫");
}
public Animal getOffspring() {
return new Animal();
}
}
class Dog extends Animal {
// 重写 makeSound() 方法
@Override // 使用注解,让编译器检查
public void makeSound() {
System.out.println("汪汪汪!");
}
// 重写 getOffspring() 方法,使用协变返回类型 (返回Dog, 是Animal的子类)
@Override
public Dog getOffspring() {
return new Dog();
}
}
class Cat extends Animal {
// 重写 makeSound() 方法
@Override
public void makeSound() {
System.out.println("喵喵喵~");
}
}
四、总结
重载:同类名同,参数不同。(发生在同类,方法名一样,参数必须不一样)
重写:子承父业,改头换面。(发生在父子类,方法名和参数必须一模一样,子类重写实现)
希望这个详细又通俗的解释能帮你彻底分清重载和重写!是不是豁然开朗了?