ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java继承核心拆解:extends、super、重写与多态实战指南

Java继承核心拆解:extends、super、重写与多态实战指南 继承这东西我在教新人的时候发现一个很普遍的现象很多人一开始觉得它特别简单无非就是一个extends关键字子类就能用父类的代码了。但真正往深了问比如为什么Java不支持多继承、构造器为什么不能被子类继承、继承跟组合到底怎么选能答上来的人就少了一大半。更别提面试的时候考官把“继承”跟“多态”“封装”揉在一起问很多人当场就懵了。这篇博文咱们换个轻松点的姿势借用“打工人”和“干饭人”这两个身份把Java继承的核心彻底拆开揉碎。你会搞清楚继承到底解决了什么问题、怎么用才是对的、哪些坑是必须避开的。不管你是刚学Java的小白还是准备面试的求职者这篇都能给你点实实在在的收获。先说清楚这不是一篇枯燥的API文档而是我踩过无数坑之后总结出来的实战心得。1. 为什么要继承从“打工人”到“干饭人”的生活映射1.1 没有继承的代码世界有多可怕先想象一下没有继承的Java世界是什么样。你是一家公司的HR系统开发者公司里有程序员、产品经理、设计师三种岗位。如果没有继承你可能会写出三个互相独立的类它们在结构上惊人地相似// 程序员类 public class Programmer { private String name; private int age; private double salary; private String programmingLanguage; public void work() { System.out.println(写代码); } public void rest() { System.out.println(睡觉); } } // 产品经理类 public class ProductManager { private String name; private int age; private double salary; private int projectCount; public void work() { System.out.println(画原型); } public void rest() { System.out.println(睡觉); } }你发现没有name、age、salary这三个字段以及rest()这个方法在每个类里都重复了一遍。这还只是三个岗位如果公司有十个岗位呢代码里将充斥着大量复制粘贴的产物。更要命的是有一天公司改了规定所有人的底薪都变成“基本工资加绩效”的结构你需要改动salary字段的定义——这时候你就得挨个去改十个类改漏一个线上就是一个bug。这正是继承要解决的核心问题把公共的属性和行为抽取到父类中实现代码复用降低维护成本。1.2 打工人和干饭人的角色类比标题里说“打工人”到“干饭人”咱们沿着这个思路走。假设你是一个刚入职的程序员你是一个标准的“打工人”。作为打工人你一定有工牌employeeId、有基本工资baseSalary、要遵守公司的考勤制度checkIn()。这些不是你独有的公司里每个人都这样这就是“员工”这个父类该定义的内容。但你的岗位是“程序员”你除了是个打工人你还会写代码coding()这是你区别于产品经理的地方。所以程序员类需要“继承”自员工类获得那些公共的东西同时新增自己的专属能力。到了中午,你从打工人变成“干饭人”。干饭人要去食堂打饭食堂有各种窗口——鲁菜窗口、川菜窗口、粤菜窗口。每个窗口都继承自“打饭窗口”这个父类它们都有同一个打饭流程排队取餐盘、刷卡、打菜但每个窗口打出来的具体菜不一样。这个场景用一个专业技术名词来描述就叫做方法的动态绑定Dynamic Binding——通过父类引用指向子类对象运行时决定到底执行哪个子类的打饭方法。我拿一个实际的代码例子来演示这个类比// 父类打饭窗口 public class CanteenWindow { public void serve() { System.out.println(取餐盘 - 刷卡 - 打菜); } } // 子类川菜窗口 public class SichuanWindow extends CanteenWindow { Override public void serve() { super.serve(); // 先走通用流程 System.out.println(额外加一勺辣椒); } } // 子类粤菜窗口 public class CantoneseWindow extends CanteenWindow { Override public void serve() { super.serve(); // 先走通用流程 System.out.println(清淡为主不放辣); } } // 调用场景 public class LunchDemo { public static void main(String[] args) { CanteenWindow window new SichuanWindow(); window.serve(); // 虽然是父类类型但实际执行川菜窗口的逻辑 } }运行这一段你会看到“取餐盘 - 刷卡 - 打菜”之后跟着“额外加一勺辣椒”。这就是继承搭配多态的经典用法。继承负责“是什么”的关系多态负责“怎么用”的灵活性——父类只管定规则子类负责玩出花样。1.3 继承真正解决的问题清单我把继承的好处做一个系统性的整理方便你对照理解代码复用公共的字段和方法放在父类所有子类自动获得杜绝重复代码。统一接口父类定义一套方法契约所有子类按统一风格实现调用方不用关心具体子类是谁。可扩展性新增一种岗位只需要继承父类、补上自己的特有逻辑老代码不用动。多态的基础没有继承就没有办法实现“父类引用指向子类对象”这种运行时动态绑定的机制。维护成本降低公共逻辑修改时只改父类一处所有子类同步生效。2. 继承的核心语法拆解extends、方法重写、super关键字的正确用法2.1 extends最基本的写法Java里实现继承只需要一个关键字extends。它的语义非常直白——“扩展”。子类不是父类的复制品而是在父类的基础上“扩展”出自己的新能力。// 父类Employee员工 public class Employee { protected String name; protected double baseSalary; public Employee(String name, double baseSalary) { this.name name; this.baseSalary baseSalary; } public void work() { System.out.println(name 在工作); } public double getSalary() { return baseSalary; } } // 子类Programmer程序员 public class Programmer extends Employee { private String programmingLanguage; public Programmer(String name, double baseSalary, String programmingLanguage) { super(name, baseSalary); // 调用父类构造器初始化父类的私有数据 this.programmingLanguage programmingLanguage; } public void writeCode() { System.out.println(name 用 programmingLanguage 写代码); } }使用的时候Programmer对象除了能调自己的writeCode()还能直接调用从Employee继承来的work()和getSalary()public class Main { public static void main(String[] args) { Programmer p new Programmer(张三, 15000, Java); p.work(); // 继承自父类的方法 p.writeCode(); // 子类自己的方法 System.out.println(工资 p.getSalary()); } }你注意看子类构造器里那个super(name, baseSalary)。我重点讲一下这行的作用子类构造器必须调用父类构造器这是Java的强制规则因为子类对象在创建时内部的父类部分必须先初始化完成。如果你不写编译器会自动加一个无参的super()但如果父类没有无参构造器编译就直接报错。这算是初学者第一个常见的坎。2.2 方法重写的正确姿势与隐藏规则方法重写Override是继承里最核心的机制之一。子类对父类的方法不满意可以按自己的需求重新实现但需要遵守Java定下的规则。我用一个具体场景说明——所有员工都要工作但不同岗位工作内容不一样// 父类 public class Employee { protected String name; public void work() { System.out.println(name 在处理基础事务); } } // 子类重写 public class ProductManager extends Employee { Override public void work() { System.out.println(name 在画原型、开评审会); } }这个例子里有两个关键点值得细说。第一个是Override注解它不是必须的但我强烈建议你每次都写上。它相当于告诉编译器“这个方法我是要重写父类的”如果父类根本没有对应的方法编译器会直接报错把潜在问题在编译期就暴露出来。第二个是重写的方法签名必须一致方法名、参数列表、返回类型或返回类型的子类型都得匹配否则编译器认为你是在定义一个新方法而不是重写。这里有一个非常隐蔽的坑我当年踩过一次。重写方法时访问权限不能比父类更严格。比如父类方法是public子类只能写成public父类方法是protected子类可以写成protected或public。我见过有同学把父类的public方法重写成了protected结果编译报错还一头雾水。原因很简单子类对象肯定要能替代父类对象使用这叫做里氏替换原则如果子类的访问权限突然收紧那原本能调父类方法的代码在传子类对象时就会直接方法访问失败。重写方法的规则我总结成一张速查表你可以直接存下来规则项要求违反后果方法签名方法名、参数列表必须完全一致变成重载Overload不是重写返回类型必须是父类返回类型或其子类型Java 5支持协变返回编译报错访问修饰符不能比父类更严格编译报错抛出的异常不能抛出比父类更宽泛的受检异常编译报错static方法不能重写可以被隐藏不是重写是多态失效final方法不能重写编译报错2.3 super不只是调用父类构造器很多教程把super讲得神神秘秘其实拆开就三个用途。第一是调用父类构造器这个前面已经说过了它必须出现在子类构造器的第一行。第二是访问父类的成员变量当子类和父类有同名变量时用super.变量名明确指定访问父类的那个。第三是调用父类被重写的方法即super.方法名()。看一个综合例子public class Parent { protected String message 父类的消息; public void printMessage() { System.out.println(message); } } public class Child extends Parent { protected String message 子类的消息; Override public void printMessage() { System.out.println(当前访问的是 this.message); // 子类的 System.out.println(父类里存的是 super.message); // 父类的 super.printMessage(); // 执行父类原始方法 } }这个例子里super.message和super.printMessage()分别演示了访问父类字段和调用父类方法。实际项目中super调用父类方法最常用的场景是子类需要在父类逻辑基础上补充额外行为。比如前面食堂窗口的例子子类先调super.serve()走通用流程再增加自己的特色。这是一种推荐做法比完全重写更优雅也更方便后续维护——公共逻辑变了子类自动跟着变。2.4 构造器与初始化顺序到底怎么走继承体系下的对象初始化是有严格顺序的这个顺序面试特别喜欢考。我直接给结论再用代码验证父类静态代码块 - 子类静态代码块 - 父类实例代码块 - 父类构造器 - 子类实例代码块 - 子类构造器public class Parent { static { System.out.println(1-父类静态代码块); } { System.out.println(3-父类实例代码块); } public Parent() { System.out.println(4-父类构造器); } } public class Child extends Parent { static { System.out.println(2-子类静态代码块); } { System.out.println(5-子类实例代码块); } public Child() { System.out.println(6-子类构造器); } } // 执行 new Child() // 输出顺序1-父类静态代码块 - 2-子类静态代码块 - 3-父类实例代码块 - 4-父类构造器 - 5-子类实例代码块 - 6-子类构造器这个顺序背后的逻辑其实不难理解静态成员属于类类加载时先初始化且只初始化一次创建对象时必须先有父类部分才能有子类部分所以父类的实例化永远在子类之前。我建议你在IDE里跑一下这个例子亲眼看看输出比死记硬背有用得多。3. 继承体系下的访问控制谁能看到谁的数据3.1 Java四个访问修饰符在继承中的影响Java有四个访问修饰符public、protected、default包私有、private。它们在继承中的表现差异很大很多人在这个点上栽过跟头。package com.example.parent; public class Base { public void methodPublic() { } protected void methodProtected() { } void methodDefault() { } // 包私有没有修饰符 private void methodPrivate() { } }当一个子类在其他包继承这个Base时情况是这样的methodPublic()子类可以访问也可以重写。methodProtected()子类可以访问也可以重写。这是protected设计的目的——把继承权限开放给子类但不开放给所有类。methodDefault()子类不能访问因为没有加修饰符的成员只能在同一包内可见。哪怕你继承了整个类这个包私有方法跟你不相干。methodPrivate()子类看不到也不能重写。私有成员完全属于父类自己子类想访问父类的私有字段唯一途径是通过父类提供的public或protected的 getter/setter 方法。有个细节值得单独说protected的跨包访问只有通过继承关系才能“间接”访问。比如子类可以super.methodProtected()调用父类受保护的方法但如果子类代码里创建一个父类对象用这个父类对象去调methodProtected()是不允许的。因为protected保护的是继承链的访问权而不是对某个对象实例的公开访问权。3.2 private成员真的不能继承吗关于这个问题网上有两种说法一种说“private成员也能被继承只是子类访问不到”一种说“private成员不能被继承”。我在实际教学中的理解是private成员确实存在于子类对象的内存结构里但它们对子类代码完全不可见从“可用性”角度讲相当于没有被继承。怎么理解“存在于子类对象里”因为前面讲过创建子类对象时会先调用父类构造器父类的私有字段就是在这个过程中被初始化的它们作为对象的一部分占据内存。只不过这些字段的“访问钥匙”只在父类手里子类想读或者写只能通过父类暴露的public/protected方法间接操作。这实际上是封装和继承共同作用的体现继承让你复用父类的数据管理逻辑封装让你不能随意绕过父类的数据校验规则。我举个实际场景帮你理解父类有个私有字段age它的setter方法里有年龄合法性校验必须是0到150之间的整数。子类如果直接继承并随意修改age校验逻辑就形同虚设。通过私有字段加受保护方法的方式父类就能牢牢控制数据的赋值规则这正是设计者想要的效果。public class Person { private int age; // 私有字段子类拿不到 protected void setAge(int age) { if (age 0 || age 150) { throw new IllegalArgumentException(年龄不合法); } this.age age; } protected int getAge() { return this.age; } } public class Student extends Person { public void init() { setAge(20); // 通过父类的protected方法间接操作私有字段 System.out.println(学生年龄 getAge()); } }这种设计模式在真实框架里非常常见——父类把核心字段设为私有用受保护的方法暴露受控的读写入口。子类拥有操作能力但没有绕过校验的权力。4. 继承的高级话题为什么Java不支持多继承抽象类和接口怎么选4.1 单继承的底气在哪里Java官方设计时明确要求一个类只能有一个直接父类这和C支持多继承形成了鲜明对比。原因非常实际——多继承会带来著名的“菱形问题”。菱形问题的场景是这样的假设有两个类 A 和 B它们都继承自一个公共基类然后有一个类 C 同时继承 A 和 B。此时 B 中有一个方法foo()A 中也继承了这个foo()那 C 到底继承谁的编译器没法判断逻辑上也会出现数据冗余——C 里会有两份公共基类的数据副本。这个问题的复杂度对语言设计者和开发者都极高处理不好就是一团乱麻。Java选择单继承从语言层面直接消灭了这一问题。那如果确实需要来自多个来源的“能力”怎么办Java给出了替代方案——接口的多实现。一个类只能有一个父类但可以实现多个接口接口之间可以多继承接口里只有方法声明和默认方法没有实例字段和构造器所以接口的“多继承”不会产生菱形问题中的状态冲突。// 接口A public interface Writer { void write(); } // 接口B public interface Runner { void run(); } // 类可以同时实现多个接口 public class Developer implements Writer, Runner { Override public void write() { System.out.println(写技术文档); } Override public void run() { System.out.println(下班跑步); } }这里Developer同时获得了来自两个接口的“能力”但两者之间没有任何数据状态冲突因为接口不保存状态。如果你在Java 8之后使用接口的默认方法default方法会发现接口已经能写出一些包含逻辑的方法了真的是越来越强大了。4.2 抽象类介于普通类和接口之间的桥梁抽象类abstract class用一句话概括就是可以包含未实现方法的类。它允许你只声明方法不写实现由子类去完成同时它又能拥有字段和已实现的方法。这种“半成品”特性让它成为模板方法模式的主力。public abstract class AbstractDataLoader { // 抽象方法必须由子类实现 protected abstract String loadData(); // 普通方法子类可以直接复用 public void printLoadedData() { String data loadData(); System.out.println(加载到的数据是 data); } } // 子类A从数据库加载 public class DbDataLoader extends AbstractDataLoader { Override protected String loadData() { return 从数据库加载的数据; } } // 子类B从文件加载 public class FileDataLoader extends AbstractDataLoader { Override protected String loadData() { return 从文件加载的数据; } }这个场景很典型AbstractDataLoader把通用流程printLoadedData()写好了把不确定的loadData()留给子类实现。这样父类定义算法骨架子类定制具体步骤代码结构一目了然。我用这个模式重构过很多业务场景尤其是“流程固定、步骤多变”的逻辑比如数据导入、报表生成、消息发送——先抽象公共流程再让每个具体场景去实现差异部分维护起来极其舒服。4.3 继承、抽象类、接口傻傻分不清一张表讲透很多初学者分不清这三者的使用场景。我整理了一个对比表你看完基本就能在写代码时做出正确选择了。维度类继承抽象类接口关键字extendsextendsimplements一个类能拥有几个1个1个多个实例字段可以有可以有不可以有仅常量构造器可以有可以有不可以有已实现方法都可以可以有可以有default方法未实现方法不允许可以有可以有核心语义“是什么”的强关系模板化的“部分完成”“能做什么”的能力契约使用场景明确的is-a关系子类共享代码且模板流程固定定义行为规范能力混搭我给一个简单的选择流程如果两个类的关系是“A是一种B”比如程序员是一种员工用继承如果只想复用一部分代码但不太确定关系的方向优先考虑组合而不是继承如果只是想让多个无关的类拥有某种共同能力比如都能被序列化、都能跑起来用接口如果既想复用代码又要约束子类必须实现某些方法且这些子类之间有明确的父子概念用抽象类。4.4 构造器为什么不能被子类继承这个问题经常出现在面试题里。先说结论构造器不是成员所以不存在继承一说。构造器看起来像方法但它的特殊之处在于构造器的名字必须和类名完全一致。子类的类名和父类不同如果构造器能被继承那子类里就会出现一个名字跟子类类名不一样的“构造函数”这在语义上就乱套了。更本质的原因是构造器的职责是“创建一个具体类型的对象”。父类构造器创建的是父类对象子类构造器创建的是子类对象两种对象的内部结构都不一样。Java的解决方案是子类构造器通过super(...)调用父类构造器让父类先把自己该初始化的数据准备好然后子类构造器再去补充子类特有的数据。从这一点看构造器虽然没有被继承但子类可以通过super把父类的初始化逻辑“复用”过来。这也是为什么我们说继承让代码复用但不代表子类能直接“调用”父类构造器——它是在子类构造过程中被间接触发的。5. 实战进阶继承与多态的协同工作5.1 向上转型把子类当成父类用多态在代码里的典型体现是向上转型即把子类对象赋值给父类类型的引用。这种操作之所以安全是因为子类本质上就是一种父类——程序员确实是一个员工所以把程序员对象当成员工用完全没问题。Employee e new Programmer(李四, 20000, Python); e.work(); // 调用的是 Programmer 重写后的 work()这句e.work()是关键。编译时编译器看到e的类型是Employee检查Employee类里确实有work()方法于是编译通过运行时JVM发现e实际指向的是一个Programmer对象于是执行了Programmer里重写后的work()。这个“编译看左边运行看右边”的机制就是动态绑定的精髓。这种写法最大的价值是面向抽象编程。你可能要问了这样写和直接Programmer p new Programmer(...)有什么区别区别大了。看这个例子public class Company { // 没有多态的写法 public void manageProgrammer(Programmer p) { p.work(); } public void manageProductManager(ProductManager pm) { pm.work(); } public void manageDesigner(Designer d) { d.work(); } // 有多态的写法 public void manage(Employee e) { e.work(); } }如果没有多态公司每招一个新岗位Company类就要新增一个管理方法。而有了多态之后Company.manage(Employee e)这一个方法就能管理所有员工子类。新增岗位时只需要让新类继承Employee并重写work()Company的代码一行都不用改。这就是开闭原则最直观的落地——对扩展开放对修改关闭。5.2 向下转型如何安全地把父类变回子类与向上转型相对的是向下转型。比如你有一个Employee的引用它实际指向的是Programmer但你想要调用Programmer特有的writeCode()方法这时候就需要向下转型。Employee e new Programmer(王五, 18000, Java); // 向下转型 if (e instanceof Programmer) { Programmer p (Programmer) e; p.writeCode(); }向下转型是不安全的因为父类引用并不一定指向子类对象。比如你写Employee e new Employee(...)非要强转成Programmer运行时会抛出ClassCastException。所以在向下转型前必须用instanceof关键字检查一下。这里有个Java 16之后的新特性可以简写就是instanceof模式匹配if (e instanceof Programmer p) { // 转型后的 p 直接可用 p.writeCode(); }这行代码相当于把类型检查和类型转换一步到位了代码会清爽很多。实际开发里我建议能避免向下转型就尽量避免——如果调用方需要子类特有方法那大概率说明接口设计不够完善优先考虑把公共方法提升到父类或接口层。5.3 继承与组合真正的“组合优于继承”最后必须聊一个很多进阶程序员容易忽略的话题——组合优于继承。我们可以这样理解继承关系继承建立的是一种is-a关系比如程序员是一个员工组合建立的是一种has-a关系比如汽车有一台发动机。有些场景确实不适合用继承我举个例子。假设你要创建一个Vehicle交通工具类然后创建一个Car子类。看起来没问题但如果你需要给Car添加一个Engine的行为用继承就会出问题——Engine不是一种Vehicle而是Vehicle的一部分。这时候应该用组合public class Engine { public void start() { System.out.println(引擎启动); } } public class Car { private Engine engine; // 组合 public Car() { this.engine new Engine(); } public void start() { engine.start(); // 通过组合调用 } }继承的缺点在于它把父子类关系绑得太死子类无法在运行期改变父类而且父类的任何改动都可能影响子类。组合则灵活很多你可以随时替换组件对象不必动整个类的结构。所以我的经验准则是除非十分确定是纯粹的is-a关系否则默认优先用组合。这个原则在Java标准库里也体现得很明显——HashMap内部组合了Node数组ArrayList内部组合了Object[]数组它们都不是靠继承来复用数组的功能。6. 继承高频面试题与避坑清单实录6.1 面试官最爱问的继承问题我结合自己和身边同事的真实面试经历整理了下面几道高频题目每题都给出了踩坑提醒。第一题重写和重载的区别是什么这是个超高频题。重载Overload发生在同一个类里方法名相同但参数列表不同跟返回类型、访问修饰符无关重写Override发生在父子类之间方法签名要完全一致。考这道题时你最好主动补充一句重载是编译期的静态绑定重写是运行期的动态绑定。第二题构造器能被重写吗不能因为构造器不是成员而且构造器名字必须和类名一致子类类名跟父类不同自然无法重写。第三题静态方法能被重写吗不叫重写准确说法是“隐藏”。子类定义同名同参数的静态方法时父类的静态方法并没有被真正覆盖只是被“隐藏”了。具体表现是通过子类引用调的是子类的静态方法通过父类引用调的是父类的静态方法没有动态绑定。public class Parent { public static void staticMethod() { System.out.println(父类静态方法); } } public class Child extends Parent { public static void staticMethod() { System.out.println(子类静态方法); } } // 测试 Parent p new Child(); p.staticMethod(); // 输出父类静态方法 Child c new Child(); c.staticMethod(); // 输出子类静态方法看到没静态方法的调用跟随引用类型而不是实际对象类型。这一点经常被拿来出题也是初学者最容易掉进去的坑。第四题Object类在继承体系里有什么用在Java中所有类直接或间接继承自Object。如果定义一个类时不写extendsJava编译器会自动加上extends Object。这意味着Object里的toString()、equals()、hashCode()、getClass()等方法所有对象都能用。这也是为什么你可以对任意对象调用toString()而不会编译报错。第五题继承和多态是什么关系继承是多态的前提没有继承关系父类引用就无法指向子类对象。但多态的实现还依赖方法重写和动态绑定。可以说继承是“结构基础”重写是“行为入口”动态绑定是“运行机制”三者合力才能实现真正意义上的多态。6.2 我踩过的继承相关的坑第一个坑重写方法时忘了加 Override。有一次我把参数类型写错了Object obj写成了String str本意是想重写equals方法结果变成了重载程序在运行时执行了错误的逻辑分支排查了两个小时才找到原因。后来我养成习惯重写方法一律加Override让编译器帮我把关。这个习惯帮我省了无数时间。第二个坑没有优雅地处理深继承导致代码蔓延的坑。曾经有个老项目业务逻辑里套了四层继承最顶层改了一个方法下面的三层子类全部受影响测试时崩出好几个隐藏 bug。后来我反思继承层级最好控制在两层以内超过两层的继承体系会让逻辑变得极难追踪。遇到这种情况更适合用组合或者接口来重构。第三个坑父类构造器抛异常时子类的处理。如果父类构造器声明了受检异常子类构造器同样必须处理因为子类构造器第一行调用了super(...)异常会顺着传递上来。这个坑在写文件读取、网络连接相关的继承类时特别常见。我的建议是父类构造器的受检异常尽量在父类内部兜底处理避免把异常处理压力传导给所有子类。第四个坑没有考虑equals()的对称性问题。子类重写equals()时如果只判断了子类类型会破坏对称性——parent.equals(child)为true但child.equals(parent)却是false。一个稳妥的做法是在使用继承的类中先判断是否能转成父类再判断类型是否一致或者干脆在equals()里先用getClass()做严格类型比较。不过严格类型比较之后子类和父类就永远不相等了这又涉及业务语义的选择没有唯一标准答案。我的建议是在写继承体系时尽量少重写equals()如果必须重写一定要意识到对称性这个隐患。6.3 避坑清单继承代码的黄金法则记得给重写方法加 Override 注解让编译器帮你检查签名是否正确。不要为了复用代码而滥用继承优先考虑组合。只有明确的is-a关系才适合继承。控制继承深度我个人的上线是三层。再多就该重构了。父类中公共逻辑的修改要极度谨慎因为一个改动会波及所有子类。使用 protected 而不是包私有如果成员需要被子类访问。不要在构造器里调用可重写的方法。这个坑特别隐蔽父类构造器先执行如果它调用了被子类重写的方法此时子类字段还没初始化可能会拿到默认值null或0导致神秘的空指针或错误逻辑。我推荐在构造器中只调用private或final方法。尽量面向接口或父类编程而不是面向具体实现编程这样代码的扩展性会好很多。7. 用一句话复盘继承的底层逻辑我个人在实际教学中发现真正能让我记住继承本质的不是一堆概念背诵而是一句足够简洁的话继承的本质是抽取公共特征并建立类型间的关联它决定了“我是什么”多态则决定了“我怎么用我”。你拿到一个新需求先分析哪些是公共的、哪些是可变的、哪些是不变的然后把公共的放到父类把可变的定义成抽象方法让子类实现把不变的直接写在父类里——这样一个清晰的继承设计就成型了。回到标题里的“打工人”到“干饭人”其实就是一个最简单直白的场景打工人是所有人的公共身份干饭人因人而异各有吃法。继承让所有员工都能领到基本工资公共字段重写让每个岗位都有自己的“干饭方式”方法实现动态绑定让公司管理系统只面向父类编程就能一视同仁地管理所有人多态。这套思路打通了Java面向对象的半壁江山基本就拿下了。最后再分享一个小技巧如果你觉得自己对继承还是不熟不要去背理论而是找一个你手头最常用的工具类比如ArrayList去翻一翻它的继承体系——ArrayList继承了什么、实现了哪些接口、每个父类和接口分别带来了什么能力。把一个真实类的继承关系吃透比刷十道概念题都管用。这是我在带团队时反复推荐的做法实测效果极好。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进