方法重写(Override)是Java面向对象编程中实现多态性的关键机制,也是继承体系中最核心的概念之一。本文将全面剖析方法重写的方方面面,包括语法规则、实现原理、使用场景以及常见误区,并通过丰富的代码示例展示方法重写的各种细节。
一、方法重写的基本概念
1.1 什么是方法重写?
方法重写是指子类重新定义父类中已经定义的方法,以改变或扩展该方法的行为。重写后的方法:
具有相同的方法签名(方法名+参数列表)提供不同的方法实现在运行时根据对象实际类型调用相应版本
1.2 方法重写 vs 方法重载
特性方法重写(Override)方法重载(Overload)方法签名必须相同必须不同返回类型相同或协变可以不同访问修饰符不能更严格可以不同抛出异常不能更多或更宽可以不同调用方式运行时确定(动态绑定)编译时确定(静态绑定)应用场景父子类之间同一类中1.3 简单示例
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
public Animal getOffspring() {
return new Animal();
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪");
}
@Override
public Dog getOffspring() { // 协变返回类型
return new Dog();
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 多态
myAnimal.makeSound(); // 输出"汪汪汪"
Animal offspring = myAnimal.getOffspring();
System.out.println(offspring.getClass()); // 输出"class Dog"
}
}
二、方法重写的详细规则
2.1 基本规则
方法签名必须完全相同(方法名和参数列表)返回类型可以是父类方法返回类型的子类(协变返回类型,Java 5+)访问修饰符不能比父类方法更严格:
父类public → 子类必须public父类protected → 子类protected或public父类默认(包私有) → 子类不能是private
不能抛出比父类方法更多的检查异常(可以抛出更少或子类异常)不能重写final、static或private方法
2.2 协变返回类型(Covariant Return Type)
Java 5开始支持协变返回类型,允许重写方法返回父类方法返回类型的子类:
class Fruit {
protected Number getWeight() {
return 100;
}
}
class Apple extends Fruit {
@Override
public Integer getWeight() { // Integer是Number的子类
return 150;
}
}
2.3 异常处理规则
class Parent {
protected void process() throws IOException {
// ...
}
}
class ValidChild extends Parent {
@Override
protected void process() throws FileNotFoundException { // FileNotFoundException是IOException的子类
// ...
}
}
class InvalidChild extends Parent {
@Override
protected void process() throws Exception { // 编译错误:Exception比IOException更宽
// ...
}
}
2.4 @Override注解
@Override注解不是必须的,但强烈建议使用:
帮助编译器检查是否确实重写了父类方法提高代码可读性防止因拼写错误导致意外创建新方法
class Bird {
public void fly() {
System.out.println("鸟儿飞翔");
}
}
class Penguin extends Bird {
@Override
public void fly() {
System.out.println("企鹅不会飞");
}
// 如果没有@Override,下面的拼写错误不会被发现
// public void flay() { // 本意是重写fly,但拼写错误
// System.out.println("拼写错误的方法");
// }
}
三、方法重写的实现原理
3.1 虚方法表(Virtual Method Table)
JVM通过虚方法表实现动态绑定:
每个类都有一个虚方法表表中存放着方法的实际入口地址子类方法表中:
重写的方法指向子类实现未重写的方法指向父类实现
Animal虚方法表:
+-------------------+-------------------+
| 方法签名 | 实际地址 |
+-------------------+-------------------+
| makeSound() | Animal.makeSound()|
| getOffspring() | Animal.getOffspring()|
+-------------------+-------------------+
Dog虚方法表:
+-------------------+-------------------+
| 方法签名 | 实际地址 |
+-------------------+-------------------+
| makeSound() | Dog.makeSound() |
| getOffspring() | Dog.getOffspring()|
+-------------------+-------------------+
3.2 静态绑定 vs 动态绑定
静态绑定:编译时确定方法调用(private、static、final方法和字段访问)动态绑定:运行时根据对象实际类型确定方法调用(实例方法的重写)
class BindingExample {
static class Parent {
String field = "父类字段";
static void staticMethod() {
System.out.println("父类静态方法");
}
final void finalMethod() {
System.out.println("父类final方法");
}
void dynamicMethod() {
System.out.println("父类动态方法");
}
}
static class Child extends Parent {
String field = "子类字段";
static void staticMethod() {
System.out.println("子类静态方法");
}
// 不能重写final方法
@Override
void dynamicMethod() {
System.out.println("子类动态方法");
}
}
public static void main(String[] args) {
Parent obj = new Child();
System.out.println(obj.field); // 输出"父类字段"(静态绑定)
obj.staticMethod(); // 输出"父类静态方法"(静态绑定)
obj.finalMethod(); // 输出"父类final方法"(静态绑定)
obj.dynamicMethod(); // 输出"子类动态方法"(动态绑定)
}
}
四、方法重写的特殊场景
4.1 静态方法"重写"
静态方法不能被重写,只能被隐藏:
class StaticParent {
static void show() {
System.out.println("父类静态方法");
}
}
class StaticChild extends StaticParent {
static void show() { // 不是重写,是隐藏
System.out.println("子类静态方法");
}
}
public class Test {
public static void main(String[] args) {
StaticParent parent = new StaticChild();
parent.show(); // 输出"父类静态方法"(编译时类型决定)
StaticChild child = new StaticChild();
child.show(); // 输出"子类静态方法"
}
}
4.2 私有方法重写
私有方法不能被重写,子类中相同签名的方法实际上是新方法:
class PrivateParent {
private void secret() {
System.out.println("父类私有方法");
}
public void callSecret() {
secret();
}
}
class PrivateChild extends PrivateParent {
// 这不是重写,而是新方法
private void secret() {
System.out.println("子类私有方法");
}
public void callChildSecret() {
secret();
}
}
public class Test {
public static void main(String[] args) {
PrivateChild child = new PrivateChild();
child.callSecret(); // 输出"父类私有方法"
child.callChildSecret(); // 输出"子类私有方法"
}
}
4.3 构造方法中的重写陷阱
在构造方法中调用可重写方法是危险的做法:
class ConstructorParent {
ConstructorParent() {
printMessage(); // 危险:调用可重写方法
}
void printMessage() {
System.out.println("父类消息");
}
}
class ConstructorChild extends ConstructorParent {
private String message = "子类消息";
@Override
void printMessage() {
System.out.println(message); // 此时message还未初始化
}
}
public class Test {
public static void main(String[] args) {
new ConstructorChild(); // 输出"null"而非"子类消息"
}
}
问题原因:父类构造方法执行时,子类字段还未初始化
解决方案:
避免在构造方法中调用可重写方法如果必须调用,将方法声明为final或private
五、方法重写的最佳实践
5.1 遵循里氏替换原则(LSP)
子类重写方法时应当:
不改变父类方法的契约(前置条件、后置条件、不变式)不强加新的异常要求不削弱父类方法的承诺
反面例子:
class Account {
/**
* 从账户取款
* @param amount 取款金额,必须>0
* @return 取款后的余额
* @throws IllegalArgumentException 如果amount<=0
*/
BigDecimal withdraw(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("取款金额必须大于0");
}
// 实现逻辑
}
}
// 违反LSP的重写
class OverdraftAccount extends Account {
@Override
BigDecimal withdraw(BigDecimal amount) {
// 允许amount<=0(违反前置条件)
// 可能返回负余额(违反后置条件)
// 实现逻辑
}
}
5.2 使用模板方法模式
合理利用重写实现模板方法模式:
abstract class Beverage {
// 模板方法(final防止子类改变算法骨架)
final void prepare() {
boilWater();
brew();
pourInCup();
addCondiments();
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("烧水");
}
void pourInCup() {
System.out.println("倒入杯子");
}
}
class Coffee extends Beverage {
@Override
void brew() {
System.out.println("冲泡咖啡粉");
}
@Override
void addCondiments() {
System.out.println("加糖和牛奶");
}
}
class Tea extends Beverage {
@Override
void brew() {
System.out.println("浸泡茶叶");
}
@Override
void addCondiments() {
System.out.println("加柠檬");
}
}
5.3 文档化重写方法
使用JavaDoc的{@inheritDoc}标签继承父类文档:
class DocumentedParent {
/**
* 执行重要操作
* @param input 输入参数,不能为null
* @return 操作结果,总是正数
* @throws NullPointerException 如果input为null
*/
public int importantOperation(String input) {
// 实现
}
}
class DocumentedChild extends DocumentedParent {
@Override
/**
* {@inheritDoc}
* @return 结果范围扩大到所有整数
*/
public int importantOperation(String input) {
// 实现
}
}
六、经典案例:Java集合框架中的重写
6.1 equals和hashCode的重写
class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return 31 * name.hashCode() + age;
}
}
重写规则:
总是同时重写equals和hashCodeequals要满足自反性、对称性、传递性、一致性相等的对象必须有相同的hashCode
6.2 toString的重写
class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return String.format("Point[x=%d, y=%d]", x, y);
}
}
七、常见面试问题解析
7.1 重写和重载的区别?
(见本文1.2节对比表)
7.2 能否重写静态方法?
不能,静态方法属于类而非实例,子类中的同名静态方法只是隐藏了父类方法。
7.3 构造方法能否被重写?
不能,构造方法不是普通方法,子类构造方法必须通过super调用父类构造方法。
7.4 重写时返回类型可以不同吗?
可以,但必须是协变返回类型(子类方法返回父类方法返回类型的子类)。
八、总结
方法重写是Java多态性的核心实现机制,正确理解和运用方法重写需要注意:
语法规则:签名相同、访问不更严、异常不更宽、返回类型协变实现原理:虚方法表实现动态绑定特殊场景:静态方法隐藏、私有方法新定义、构造方法陷阱最佳实践:遵循LSP、使用模板方法、完整文档化常见应用:equals/hashCode/toString重写、集合框架扩展
记住Josh Bloch在《Effective Java》中的建议:“谨慎设计可被重写的方法,并文档化它们的实现要求”。合理使用方法重写可以创建出灵活、可扩展的系统,而滥用重写则会导致代码脆弱难维护。