1. 方法重写
方法重写(overriding)是指在子类中为父类已有的方法编写自己的版本。借此,程序在运行时(run-time)实现真正的多态。
通俗地说:
如果你有一个带方法的基类,并希望子类以自己的方式执行该方法,只需在子类中声明一个与父类方法具有相同签名的方法。当通过基类类型的引用调用该方法时,将执行对象实际类型中的方法版本。
生活中的例子。
想象你有一队动物,你让每个动物“发出声音”。对所有动物来说命令都是 makeSound(),但狗会汪汪叫,猫会喵喵叫,奶牛会哞哞叫。对代码来说,这是一次“相同方法”的调用,但结果不同——这就是重写的魔力!
重写的语法
基础示例
class Animal {
void makeSound() {
System.out.println("动物发出某种声音");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("汪!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("喵!");
}
}
这里在类 Animal 中定义了方法 makeSound()。子类 Dog 和 Cat 重写了这个方法,提供了各自的实现。
注解 @Override
在 Java 中,建议(也是一种好习惯)用 @Override 标注被重写的方法:
@Override
void makeSound() { ... }
这对代码运行不是必需的,但它可以:
- 让编译器检查你是否真的在重写方法(而不是在方法名或参数上打错)。
- 提升可读性——其他开发者一眼就能看出该方法是对父类方法的重写。
调用被重写的方法
Animal myDog = new Dog();
myDog.makeSound(); // 将输出:汪!
尽管变量类型是 Animal,它实际上指向的是一个 Dog 对象,因此会调用 Dog 类中的方法。这就是动态(后期)绑定。
2. 重写(overriding)与重载(overloading)的区别
重载(overloading)
- 发生在同一个类中(或同一层级里“相邻”的类)。
- 方法同名,但参数不同(类型、数量、顺序)。
- 方法选择发生在编译期。
void print(int x) { ... }
void print(String s) { ... }
重写(overriding)
- 发生在不同的类中:方法在父类中声明,在子类中被重写。
- 方法具有相同的名称和签名(参数与返回类型)。
- 方法选择发生在运行期(run-time),基于对象的实际类型。
对比表
| 重载(overloading) | 重写(overriding) | |
|---|---|---|
| 发生位置 | 同一个类中 | 父类与子类之间 |
| 方法名 | 相同 | 相同 |
| 参数 | 不同 | 相同 |
| 返回类型 | 可以不同 | 必须相同或为其子类型 |
| 绑定/选择时机 | 编译期 | 运行期(run-time) |
| 注解 | 不需要 | @Override(推荐) |
3. 方法重写的规则
重写很强大,但也有严格的规则。我们按顺序来说明。
方法签名必须一致
- 方法名、参数类型与顺序必须与父类方法完全一致。
- 返回类型必须一致,或是协变的(即父方法返回类型的子类型)。
协变返回类型示例:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat reproduce() { return new Cat(); } // OK,Cat 是 Animal 的子类型
}
访问修饰符
子类中重写方法的访问级别不能比父类方法更严格。 如果父类方法是 public,那么在子类中也必须保持 public。不能把它降为 protected 或 private。
示例:
class Parent {
public void greet() { }
}
class Child extends Parent {
// void greet() { } // 错误!默认访问级别(package-private,包可见)比 public 更严格
@Override
public void greet() { } // OK
}
异常
- 重写方法不能抛出父类方法声明中不存在的新的受检异常(checked exceptions)。
- 可以抛出更少或相同的异常。
示例:
class Parent {
void doWork() throws IOException { }
}
class Child extends Parent {
@Override
void doWork() throws FileNotFoundException { } // OK,FileNotFoundException 是 IOException 的子类型
// void doWork() throws SQLException { } // 错误!父类未声明 SQLException
}
静态方法不能被重写
静态方法可以被隐藏(hidden),但不能被重写。 如果你在子类中声明了一个与父类相同签名的静态方法,这并不是重写!它只是隐藏方法,而非多态。
class Animal {
static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
static void info() { System.out.println("Dog"); }
}
调用 Dog.info() 会输出 "Dog",但如果通过一个 Animal 类型的变量调用,将会调用 Animal.info()。这不是多态!
final 方法不能被重写
如果父类中的方法被声明为 final,尝试重写它会导致编译错误。
class Animal {
final void sleep() { }
}
class Dog extends Animal {
// @Override
// void sleep() { } // 错误!不能重写 final 方法
}
4. 实用示例
我们通过实践来看看重写如何工作,以及它与重载有何不同。
示例 1:类 Shape 及其子类
class Shape {
void draw() {
System.out.println("绘制图形");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("绘制圆形");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("绘制矩形");
}
}
使用多态:
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // 绘制圆形
s2.draw(); // 绘制矩形
}
}
尽管变量被声明为 Shape 类型,实际调用的却是对象所属类中的方法。
示例 2:与重载的区别
class Printer {
void print(String s) {
System.out.println("字符串:" + s);
}
void print(int n) {
System.out.println("数字:" + n);
}
}
这里两个方法都叫 print,但参数不同——这是重载,而不是重写。
5. 重写与调用父类方法(super)
有时在重写的方法里,你想先执行父类逻辑,再添加自己的逻辑。这时可以使用关键字 super。
class Animal {
void makeSound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // 调用父类方法
System.out.println("汪!");
}
}
调用 new Dog().makeSound() 将输出:
动物发出声音
汪!
动态绑定(late binding)如何工作
当你通过基类类型的引用调用方法时,Java 会在运行期查看该引用实际指向的对象类型,并调用该对象所属类中定义的方法版本。
Animal a = new Cat();
a.makeSound(); // 将调用 Cat.makeSound(),而不是 Animal.makeSound()
这正是 Java 多态的基础。
6. 这与您的应用有什么关系
在我们的教学应用中(例如员工管理系统),你可以创建一个基类 Employee,其中包含方法 work(),而子类 Manager 与 Developer 可以用各自的方式实现该方法:
class Employee {
void work() {
System.out.println("员工在工作");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("经理在管理");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("开发者在写代码");
}
}
现在你可以把所有员工存放在同一个数组或列表中:
Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
e.work(); // 每个对象都有各自的输出!
}
7. 重写方法的常见错误
错误 1:方法名或参数拼写错误。 如果你在方法名或其参数上打错,你不是在重写方法,而是在创建一个新方法。结果,多态不会生效。这就是为什么要始终使用 @Override——编译器会立刻提示你。
错误 2:使用更严格的访问修饰符。 如果父类方法是 public,而你在子类中将其声明为 protected 或不写修饰符——会得到编译错误。
错误 3:尝试重写 static 或 final 方法。 静态方法不能被重写,而 final 方法完全不允许被重写。如果你尝试这样做——编译器会阻止你。
错误 4:将返回类型改为不兼容的类型。 如果子类方法的返回类型与父类不一致(且不是其子类型),编译器将不允许你进行重写。
错误 5:添加新的受检异常(checked)。 重写方法不能抛出父类方法声明中不存在的新的受检异常。否则会产生编译错误。
错误 6:忘记使用 super。 如果在重写的方法中你想保留父类的部分行为,别忘了显式调用 super.methodName()。Java 不会替你自动调用。
现在你已经了解了方法重写的工作方式、它与重载的区别,以及它如何帮助我们在 Java 中实现多态。下一讲我们将看看如何在实践中使用多态——配合集合、数组与真实业务场景!
GO TO FULL VERSION