1. 项目概述:从“Hello World”到“Hello Polymorphism”
如果你写过Java,哪怕只是跟着教程敲过一个“Hello World”,那你大概率也见过@Override这个注解。它看起来平平无奇,就像代码里的一个注释,很多新手甚至会忽略它,觉得可有可无。但在我十多年的Java开发生涯里,我见过太多因为对“重写”理解不透彻而引发的诡异Bug:一个本该执行子类逻辑的方法,却莫名其妙调用了父类的实现;一个精心设计的扩展点,因为一个参数类型没对上,整个多态机制就失效了。这些问题追根溯源,往往都出在对Override这个核心机制的掌握上。
java+override这个组合,本质上探讨的是Java面向对象编程的基石之一——方法重写。它绝不仅仅是“子类写个和父类同名的方法”那么简单。它关系到程序的运行时行为、类的扩展性设计、框架的插件机制,甚至是面试官最爱问的“重写与重载的区别”。理解Override,是你从“能写代码”迈向“会设计代码”的关键一步。这篇文章,我会抛开教科书式的定义,从一个老码农的实战视角,带你彻底吃透Java方法重写。我们会聊清楚它到底是怎么工作的,在哪些场景下非用不可,又有哪些坑是你必须绕开的。无论你是正在准备面试的求职者,还是工作中被多态问题困扰的开发者,这篇文章都能给你带来实实在在的收获。
2. 重写(Override)的本质:运行时多态的引擎
2.1 不只是“覆盖”,而是“契约的履行”
很多人把重写简单地理解为“覆盖”,这其实不够准确。更本质的理解是,重写是子类对父类“行为契约”的一种具体履行和可能存在的特化实现。父类定义了一个方法签名(方法名、参数列表、返回类型),相当于对外宣布:“所有我的子类,都能响应这个消息(调用这个方法)”。子类通过重写,来提供这个消息的具体响应逻辑。
这里的关键在于“运行时绑定”或“动态绑定”。我们来看一个经典的、也是面试高频的例子:
class Animal { public void makeSound() { System.out.println("动物发出声音"); } } class Dog extends Animal { @Override public void makeSound() { System.out.println("汪汪汪!"); } } public class Test { public static void main(String[] args) { Animal myAnimal = new Dog(); // 父类引用指向子类对象 myAnimal.makeSound(); // 输出什么? } }输出结果是“汪汪汪!”。尽管变量myAnimal的编译时类型是Animal,但实际指向的是一个Dog对象。在运行时,Java虚拟机(JVM)会检查这个对象的实际类型(Dog),并调用该类型中重写的makeSound方法。这就是多态的核心魅力:编写代码时,我们面向父类(抽象)编程;执行代码时,实际运行的是子类(具体)的实现。这种机制极大地提高了代码的灵活性和可扩展性。
注意:
@Override注解虽然从Java 5才开始引入,但它是一个极其重要的“安全网”。它的作用是在编译期就帮你检查这个方法是否真的成功重写了父类的方法。如果你不小心写错了方法名、参数类型或返回类型,导致它并没有构成有效的重写,编译器会立即报错。这能防止你误以为重写成功了,但运行时却调用了父类方法,从而避免潜在的逻辑错误。所以,养成在重写方法上添加@Override注解的习惯,是一个非常好的编程实践。
2.2 重写的铁律:你必须遵守的规则
重写不是随心所欲的,Java语言规范为它设定了一系列严格的规则,以确保多态行为的一致性和安全性。理解这些规则,是避免踩坑的关键。
方法签名必须相同:这是重写的基石。方法名和参数列表(参数的类型、顺序、数量)必须与父类被重写的方法完全一致。哪怕参数名不同,只要类型和顺序一致,也算签名相同。
返回类型必须兼容(协变返回类型):在Java 5之前,要求返回类型必须完全相同。从Java 5开始,引入了协变返回类型的支持。这意味着子类重写方法的返回类型可以是父类方法返回类型的子类。这非常有用,尤其是在工厂方法或克隆方法中。
class Parent { public Number getValue() { return 1; } } class Child extends Parent { @Override public Integer getValue() { return 2; } // Integer是Number的子类,允许 }访问权限不能更严格:子类重写方法的访问修饰符不能比父类方法更严格。例如,父类方法是
public,子类重写时就不能是protected或private。这确保了通过父类引用调用该方法时,访问权限依然是有效的。反之则可以,即可以放宽权限(例如父类是protected,子类可以重写为public)。异常抛出规则:
- 受检异常(Checked Exception):子类重写方法可以抛出更具体(子类)的受检异常,或者不抛出任何受检异常,但不能抛出比父类方法声明更通用(父类)的新的受检异常。简单说,就是“只能减少,不能增加或扩大”。
- 非受检异常(Unchecked Exception/RuntimeException):规则相对宽松,可以自由声明。
不能被重写的情况:
final方法:声明为final的方法,表示其实现不可更改,不能被重写。static方法:静态方法属于类,不属于任何实例,因此不存在“重写”,只有“隐藏”。子类可以定义一个与父类静态方法签名相同的静态方法,但这并非多态,调用哪个方法取决于引用变量的编译时类型。private方法:私有方法在子类中不可见,因此无法重写。如果在子类中定义了一个同名同参数的private方法,那只是一个全新的、与父类无关的方法。
2.3 为什么需要重写?实战场景剖析
理解了“是什么”和“怎么用”,我们更要明白“为什么用”。重写不是语法游戏,它在实际项目中解决着关键问题。
场景一:框架与模板方法模式这是重写最经典的应用场景。很多框架(如Spring, JUnit)都大量使用模板方法模式。框架在父类(基类)中定义了算法的骨架,将一些步骤的实现延迟到子类中。
// 模拟一个简单的数据导出框架基类 public abstract class DataExporter { // 模板方法,定义了导出流程 public final void export() { prepareData(); String data = fetchData(); // 抽象方法,子类必须实现 processData(data); // 钩子方法,子类可选择重写 writeToFile(data); cleanup(); } protected abstract String fetchData(); // 子类必须重写以提供数据 protected void processData(String data) { // 默认实现为空,子类可重写以进行自定义处理 } private void prepareData() { /* 通用准备逻辑 */ } private void writeToFile(String data) { /* 通用写入逻辑 */ } private void cleanup() { /* 通用清理逻辑 */ } } // 具体实现 public class UserExporter extends DataExporter { @Override protected String fetchData() { // 从数据库获取用户数据 return "用户数据JSON"; } @Override protected void processData(String data) { // 对用户数据进行额外的加密处理 System.out.println("对用户数据进行加密..."); } }在这里,export方法是固定的算法流程,而fetchData和processData则通过重写来实现扩展。框架使用者只需关心业务数据获取和处理,无需重复编写导出流程的通用代码。
场景二:实现多态与策略模式通过父类引用指向不同的子类对象,程序可以在运行时表现出不同的行为。这在需要动态切换算法或策略时非常有用。
interface PaymentStrategy { void pay(double amount); } class CreditCardPayment implements PaymentStrategy { @Override public void pay(double amount) { System.out.println("使用信用卡支付: $" + amount); } } class PayPalPayment implements PaymentStrategy { @Override public void pay(double amount) { System.out.println("使用PayPal支付: $" + amount); } } class ShoppingCart { private PaymentStrategy strategy; public void setPaymentStrategy(PaymentStrategy strategy) { this.strategy = strategy; } public void checkout(double amount) { strategy.pay(amount); } } // 使用时 cart.setPaymentStrategy(new CreditCardPayment()); // 运行时决定支付方式 cart.checkout(100.0);虽然这里用了接口,但其思想与类继承的重写一脉相承,都是基于同一“契约”(方法签名)提供不同实现。
场景三:日志、监控等横切关注点(AOP思想雏形)有时我们想在调用某个方法前后统一加入日志记录、性能监控或事务管理逻辑。在不修改原有业务方法的情况下,可以通过继承和重写来实现一个简单的代理。
class Service { public void businessLogic() { System.out.println("执行核心业务逻辑..."); } } class LoggingService extends Service { @Override public void businessLogic() { System.out.println("[INFO] 开始执行 businessLogic"); long start = System.currentTimeMillis(); super.businessLogic(); // 调用父类原始逻辑 long end = System.currentTimeMillis(); System.out.println("[INFO] businessLogic 执行完毕,耗时: " + (end - start) + "ms"); } }当然,在生产环境中,我们更倾向于使用成熟的AOP框架(如Spring AOP),但这种通过重写添加增强逻辑的方式,有助于理解AOP的底层思想。
3. 重写(Override)与重载(Overload):孪生兄弟的清晰边界
这是Java面试的必考题,也是初学者最容易混淆的概念。很多人背下了“重写是父子类之间,重载是同一个类中”的口诀,但一到实际编码还是迷糊。我们来彻底厘清。
3.1 核心区别对比表
| 特性 | 重写 (Override) | 重载 (Overload) |
|---|---|---|
| 发生范围 | 继承关系中,子类与父类之间。 | 同一个类中,或者父子类之间(继承的方法也能被重载)。 |
| 方法签名 | 必须完全相同(方法名、参数列表)。 | 必须不同(参数列表的类型、个数、顺序至少有一项不同)。 |
| 返回类型 | 必须相同或是其子类(协变返回类型)。 | 可以不同。 |
| 访问修饰符 | 不能比父类方法更严格(可以更宽松)。 | 可以不同。 |
| 异常声明 | 不能抛出更宽泛的受检异常(可以更具体或不抛)。 | 可以不同。 |
| 调用机制 | 运行时(动态)绑定。根据对象的实际类型决定调用哪个方法。 | 编译时(静态)绑定。根据调用时提供的参数类型、数量、顺序决定调用哪个方法。 |
| 设计目的 | 实现多态,允许子类定制特定行为。 | 提供方法名的复用,用同一个方法名处理不同类型或数量的参数。 |
3.2 通过代码实例彻底理解
让我们用一个综合的例子来感受两者的不同:
class Calculator { // 重载示例:同一个类中,方法名相同,参数列表不同 public int add(int a, int b) { System.out.println("调用 int add(int, int)"); return a + b; } public double add(double a, double b) { System.out.println("调用 double add(double, double)"); return a + b; } public int add(int a, int b, int c) { System.out.println("调用 int add(int, int, int)"); return a + b + c; } // 一个将被重写的方法 public String describe() { return "我是一个计算器"; } } class ScientificCalculator extends Calculator { // 这不是重载,也不是重写。这是子类自己的新方法。 public double add(double a, double b, double c) { return a + b + c; } // 重写示例:子类中,方法签名与父类完全相同 @Override public String describe() { return "我是一台科学计算器"; } // 注意:这构成了对父类 add(int, int, int) 的重载(因为参数列表不同),而非重写。 public int add(int a, int b, int c, int d) { return a + b + c + d; } } public class TestOverloadOverride { public static void main(String[] args) { Calculator calc = new ScientificCalculator(); // 重载的调用:编译时根据参数类型决定 System.out.println(calc.add(1, 2)); // 输出:调用 int add(int, int) -> 3 System.out.println(calc.add(1.5, 2.5)); // 输出:调用 double add(double, double) -> 4.0 System.out.println(calc.add(1, 2, 3)); // 输出:调用 int add(int, int, int) -> 6 // 重写的调用:运行时根据对象实际类型决定 System.out.println(calc.describe()); // 输出:我是一台科学计算器 (尽管calc是Calculator类型) // 编译错误:Calculator类型没有add(int, int, int, int)方法 // System.out.println(calc.add(1,2,3,4)); // 需要向下转型 ScientificCalculator sciCalc = (ScientificCalculator) calc; System.out.println(sciCalc.add(1, 2, 3, 4)); // 输出:10 } }关键点解析:
add方法在Calculator类内部构成了重载,因为参数列表(类型和数量)不同。ScientificCalculator中的describe方法是对父类方法的重写,因此通过父类引用调用时,执行的是子类的实现。ScientificCalculator中的add(int a, int b, int c, int d)方法,由于参数列表与父类任何add方法都不同,因此它不是重写,而是子类自身的一个重载方法(重载了从父类继承来的add方法族)。- 通过父类引用
calc无法调用子类独有的add(int, int, int, int)方法,这体现了Java的编译时类型检查。
3.3 一个极易混淆的陷阱:静态方法“重写”
这是一个高频面试坑点。静态方法不存在重写,只有隐藏。
class Parent { public static void staticMethod() { System.out.println("Parent static method"); } public void instanceMethod() { System.out.println("Parent instance method"); } } class Child extends Parent { // 这是方法隐藏,不是重写! public static void staticMethod() { System.out.println("Child static method"); } // 这是真正的重写 @Override public void instanceMethod() { System.out.println("Child instance method"); } } public class TestStatic { public static void main(String[] args) { Parent p = new Child(); // 静态方法:看引用类型(Parent) p.staticMethod(); // 输出:Parent static method Parent.staticMethod(); // 输出:Parent static method Child.staticMethod(); // 输出:Child static method // 实例方法:看对象实际类型(Child) p.instanceMethod(); // 输出:Child instance method } }结论:静态方法的调用在编译时就确定了,只和引用变量的类型有关,与对象实际类型无关。因此,永远不要试图去“重写”静态方法,如果你在子类中定义了一个与父类签名相同的静态方法,请使用@Override注解,编译器会报错提醒你。正确的做法是,如果需要在子类中提供不同的静态行为,请换一个方法名。
4. 高级话题与实战避坑指南
掌握了基础,我们来看看一些更深入的话题和实际开发中容易踩的坑。
4.1super关键字:与父类对话的桥梁
在子类重写的方法中,有时我们并不想完全替换父类的逻辑,而是希望在父类逻辑的基础上进行增强。这时就需要用到super关键字来调用父类被重写的方法。
class Vehicle { public void start() { System.out.println("车辆启动自检..."); System.out.println("发动机点火..."); } } class ElectricCar extends Vehicle { @Override public void start() { System.out.println("电动汽车高压系统上电..."); System.out.println("电池管理系统初始化..."); super.start(); // 调用父类的start方法,复用通用启动逻辑 System.out.println("电机准备就绪..."); } }使用场景:
- 扩展功能:如上例,在父类逻辑前后添加新的操作。
- 访问被隐藏的父类字段:如果子类字段与父类字段同名,可以使用
super.field来访问父类字段。 - 在子类构造器中调用父类构造器:
super(...)必须是子类构造器的第一条语句。
实操心得:谨慎使用
super。过度依赖super调用父类具体实现,会提高子类与父类的耦合度。在设计良好的继承体系中,父类方法应该是可被安全重写的。如果一段逻辑子类必须调用,考虑将其抽取成父类的protected工具方法,或者在模板方法模式中定义为final的步骤,这样意图更清晰。
4.2 重写equals()和hashCode():对象相等的灵魂
这是重写最需要谨慎对待的领域之一。Object类提供的默认equals()方法比较的是对象引用(内存地址),这通常不符合业务逻辑。当我们说两个Student对象“相等”时,往往是指他们的学号相同。
铁律:如果你重写了equals()方法,你必须同时重写hashCode()方法。
原因:基于散列的集合类(如HashMap,HashSet,Hashtable)依赖这两个方法工作。它们有一个至关重要的约定:两个相等的对象必须具有相等的哈希码。如果只重写equals而不重写hashCode,可能会导致两个逻辑上相等的对象被放入HashSet的两个不同位置,或者作为HashMap的不同键,这完全违背了这些集合类的设计初衷。
import java.util.Objects; class Student { private String id; private String name; public Student(String id, String name) { this.id = id; this.name = name; } @Override public boolean equals(Object o) { // 1. 检查是否同一个对象 if (this == o) return true; // 2. 检查类型是否匹配 if (o == null || getClass() != o.getClass()) return false; // 3. 类型转换并比较关键字段 Student student = (Student) o; return Objects.equals(id, student.id); // 假设学号唯一标识一个学生 } @Override public int hashCode() { // 使用与equals比较相同的字段生成哈希码 return Objects.hash(id); } }为什么用Objects.hash()?它帮你处理了null值,并提供了良好的哈希值分布。在较旧版本的Java中,你可能需要手动组合字段哈希码,如31 * id.hashCode(),31是一个奇素数,可以减少哈希冲突。
4.3 重写toString():调试与日志的利器
默认的Object.toString()返回的是类名和哈希码,如Student@1b6d3586,这对调试毫无帮助。重写toString(),返回一个包含对象关键信息的字符串,能极大提升开发效率。
@Override public String toString() { return "Student{" + "id='" + id + '\'' + ", name='" + name + '\'' + '}'; }现在,当你打印一个Student对象或在调试器中查看时,会得到Student{id='S001', name='张三'}这样清晰的信息。很多IDE(如IntelliJ IDEA, Eclipse)都可以自动生成高质量的toString()方法。
4.4 构造器能重写吗?绝对不能!
这是一个原则性问题。构造器不是方法,它不能被继承,因此也绝对不能重写。子类构造器的第一行必须(显式或隐式地)调用父类的某个构造器(super(...)),以确保父类部分被正确初始化。如果父类没有无参构造器,子类构造器必须显式调用父类的有参构造器。
class Parent { private String familyName; public Parent(String familyName) { this.familyName = familyName; System.out.println("Parent构造器被调用"); } } class Child extends Parent { private String firstName; // 编译错误!因为父类没有默认无参构造器,而子类默认会调用super() // public Child(String firstName) { // this.firstName = firstName; // } // 正确做法:显式调用父类有参构造器 public Child(String familyName, String firstName) { super(familyName); // 必须放在第一行 this.firstName = firstName; System.out.println("Child构造器被调用"); } }5. 面试高频问题深度剖析
结合网络热词中的“java面试题”、“java八股文”,这里梳理几个关于Override的深度面试题及其回答思路。
问题一:重写(Override)和重载(Overload)的区别,JVM是如何处理的?
回答思路:
- 区别:从发生范围、方法签名、返回类型、访问控制、异常、绑定时机、设计目的等方面对比(如前文表格)。
- JVM处理:
- 重载:属于静态分派(Static Dispatch)。在编译阶段,Java编译器根据方法的调用者(对象)的声明类型和方法参数的静态类型来确定具体调用哪个重载版本。编译完成后,符号引用就确定了。
- 重写:属于动态分派(Dynamic Dispatch)。编译阶段,编译器只知道调用的是父类方法。在运行阶段,JVM会根据对象的实际类型(Runtime Type)在方法区中查找该类型的方法表,找到真正要执行的方法地址。这是多态性的根本实现。JVM通过虚方法表(Virtual Method Table, vtable)来高效实现这一查找过程。每个类都有一个vtable,其中列出了所有虚方法(非private, static, final的方法)的实际入口地址。子类的vtable会复制父类的vtable,并用重写的方法地址替换对应的条目。
问题二:@Override注解有什么用?不加会怎样?
回答思路:
- 作用:它是一个编译时检查注解。主要作用是声明该方法意图重写父类方法,并让编译器帮你做验证。
- 不加的后果:
- 潜在Bug:如果你本想重写,但手误写错了方法名、参数类型或返回类型,导致它变成了一个全新的方法(重载或无关方法)。编译器不会报错,但程序运行时不会执行你期望的多态逻辑,Bug非常隐蔽。
- 代码可读性:加上
@Override能让阅读代码的人立刻明白这是重写方法,而不是子类新增的方法。
- 结论:强烈建议在所有意图重写的方法上都加上
@Override注解。这是一个低成本、高收益的最佳实践。
问题三:哪些方法不能被重写?为什么?
回答思路:
final方法:final关键字本身就表示“不可更改”。用于方法时,表示该方法实现是稳定的、不允许子类修改的,常用于设计或安全目的。static方法:静态方法属于类,与任何实例无关。它的调用在编译时根据类名确定,不具备多态性所需的“运行时根据对象类型决定”的特性。private方法:私有方法在子类中不可见,子类根本不知道它的存在,因此无从重写。子类中定义的同名private方法只是一个巧合,与父类无关。- 构造器:构造器用于初始化对象,不是成员方法,不能被继承,因此也不能被重写。
问题四:重写方法时,访问修饰符、返回类型和异常声明有什么限制?
回答思路:
- 访问修饰符:子类方法的访问权限不能低于父类方法(即不能更严格)。这是里氏替换原则(LSP)的要求——任何使用父类对象的地方,都应该能透明地替换为子类对象。如果子类方法访问权限更小(如父类
public,子类protected),那么通过父类引用调用该方法就可能失败,破坏了替换原则。反之,放宽权限(如protected->public)是允许的。 - 返回类型:Java 5+支持协变返回类型。子类方法的返回类型可以是父类方法返回类型的子类。这提供了更精确的类型信息,非常有用。
- 异常声明:
- 受检异常:子类方法可以抛出更具体的异常,或者完全不抛,但不能抛出新的、更通用的受检异常。这保证了调用父类方法的代码(捕获了父类声明的异常)在调用子类方法时,异常处理仍然是安全的。
- 非受检异常(RuntimeException):无此限制,可以自由声明。
6. 从设计模式看重写的威力:策略模式与模板方法模式
理解重写,不能只停留在语法层面,更要看到它在优秀软件设计模式中的应用。这里我们简要剖析两个重度依赖重写的模式。
模板方法模式(Template Method Pattern)前文在框架场景中已简要提及。它定义了一个操作中的算法骨架,而将一些步骤延迟到子类中实现。父类的模板方法(通常是final的)定义了不变的流程,其中调用的抽象方法或可重写的钩子方法(Hook Method)则由子类重写以提供具体实现。java.util.AbstractList,javax.servlet.http.HttpServlet(如doGet,doPost)都是此模式的经典应用。重写在这里是实现算法步骤可变部分的核心机制。
策略模式(Strategy Pattern)它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户。通常通过接口来定义策略,不同的策略实现类重写接口方法。虽然常用接口,但其思想与基于继承的重写一脉相承。客户端持有一个策略接口的引用,在运行时可以注入不同的策略实现对象,从而改变行为。这完美体现了“针对接口编程,而非实现编程”和“开闭原则”。
设计原则关联:重写是实现“开闭原则(Open-Closed Principle)”和“里氏替换原则(Liskov Substitution Principle)”的关键技术手段。开闭原则要求对扩展开放,对修改关闭。通过继承和重写,我们可以在不修改父类代码的情况下,扩展出新的行为。里氏替换原则要求子类对象必须能够替换掉所有父类对象。正确地进行方法重写(遵守访问权限、异常等规则),是满足这一原则的基本保障。
7. 常见问题排查与最佳实践
7.1 我加了@Override,但编译器报错“Method does not override a method from its superclass”?
这是最常见的编译错误之一。原因和排查步骤:
- 检查方法签名:确保方法名、参数类型、参数顺序、参数数量与父类方法完全一致。大小写错误、参数顺序调换都是常见原因。
- 检查父类方法是否存在且可访问:确认父类中确实有这个方法,并且它不是
private、static或final的。 - 检查返回类型:如果不支持协变返回类型(Java 5之前),返回类型必须严格相同。如果支持,检查子类返回类型是否是父类返回类型的子类。
- 检查泛型擦除:在涉及泛型时容易出问题。例如,父类方法是
void setData(List<String> list),子类试图重写为void setData(List<Integer> list)。由于泛型擦除,编译后两者签名都是void setData(List list),导致子类方法被视为重复定义而非重写。需要使用桥接方法或调整设计。 - 检查是否在实现接口:如果类实现了一个接口,
@Override也可以用于检查是否正确地实现了接口方法。此时需核对接口中的方法定义。
7.2 为什么我的重写方法没有被调用?(多态失效)
症状:通过父类引用调用方法,期望执行子类重写逻辑,但实际执行了父类逻辑。 排查:
- 确认对象实际类型:使用
instanceof或直接打印object.getClass().getName(),确保引用实际指向的是子类对象,而不是父类对象。 - 确认方法是否被真正重写:检查子类方法签名是否完全匹配,并添加了
@Override注解看是否报错。 - 检查方法访问权限:确保子类方法的访问权限不低于父类。如果父类是
package-private(默认),子类在不同包中重写时,只能使用public或protected,不能使用默认权限。 - 是否是静态方法?静态方法调用看引用类型,不是多态。
7.3 最佳实践清单
- 始终使用
@Override注解:这是最重要的习惯,能避免大量低级错误。 - 遵守重写规则:严格遵守访问权限、返回类型、异常声明的规则,确保符合里氏替换原则。
- 调用
super方法要谨慎:明确调用super.method()的目的是扩展行为,而非改变核心逻辑。如果发现频繁需要调用super,考虑是否应该使用组合而非继承。 - 重写
equals()必须重写hashCode():这是一条铁律,务必遵守。 - 重写
toString():为重要的业务类重写toString(),便于调试和日志记录。 - 考虑使用
final保护不被重写:如果某个方法的设计不允许被子类修改,将其声明为final。 - 优先使用组合而非继承:继承是一种强耦合关系。如果只是为了复用代码,优先考虑组合(持有对象)而非继承。重写应主要用于表达“是一个(is-a)”的关系和多态行为。
- 为继承而设计,否则禁止继承:如果一个类不是专门为了被继承而设计的,最好将其声明为
final,或者至少将它的所有方法都声明为final,防止不可控的子类化带来的问题。
方法重写是Java面向对象编程的精华所在,它连接了继承与多态,是构建灵活、可扩展系统的基石。从最初的语法规则记忆,到理解其背后的JVM动态分派机制,再到在设计模式中体会其精妙运用,这是一个不断深化的过程。希望这篇来自一线实战的总结,能帮你把Override这个关键词,从面试八股文变成你手中游刃有余的设计工具。记住,写出能编译通过的代码只是开始,写出符合面向对象设计原则、易于维护和扩展的代码,才是我们不断追求的目标。下次当你写下@Override时,不妨多想一步:我这样设计,是否真的符合“开闭原则”?是否能让我的代码在未来更容易应对变化?