☰
Java面向对象三大特性:封装、继承、多态原理与实战指南
2026/10/6 4:47:31 网站建设 项目流程

你是第1次要求我按照这份规范来写,那我也按实战博主的调性,把这篇 Java 三大特性拆开揉碎了讲。最近不管是准备校招、跳槽面试,还是在团队里做 code review,只要聊到 Java 基础,封装、继承、多态这三个词必定出现。但说句实在话,能把它们挂嘴上的人很多,能讲清楚"为什么要这么设计""底层是怎么运行的""在项目里该怎么落地"的人真不多。这篇文章就从一个老开发的角度,把这三个必考点彻底说透,顺便把面试里那些容易被追问的细节一起梳理掉。

1. 先搞懂面向对象到底在解决什么问题

1.1 从面向过程到面向对象:思维模式的转变

很多初学者一开始接触 Java 时会困惑:我写 C 语言的时候,函数一把梭也挺好的,为什么非要搞出类、对象这些花活?其实面向对象不是矫情,它是在软件规模变大之后被逼出来的产物。

面向过程的思维方式是"我要做什么",于是把一个大任务拆成一个个函数,数据到处传。一个商城系统如果全用函数写,订单数据、用户数据、商品数据全散落在各个函数参数里,改一个数据结构可能牵一发动全身。而面向对象的思维方式是"谁来做这件事",把数据和操作数据的方法绑在一起,形成一个对象。订单对象自己知道自己怎么算总价,用户对象自己知道怎么校验权限,这样代码的边界自然就清晰了。

封装、继承、多态这三个特性,其实就是面向对象这套方法论在 Java 语言层面的三个支撑点。封装解决的是"每个对象内部的边界和责任",继承解决的是"对象之间的共性抽取和复用",多态解决的是"同一行为在不同对象身上的差异化表现"。三者各管一摊,但又互相配合,合在一起才有了 Java 代码那种"结构清晰、便于维护"的底子。

1.2 三大特性不是孤立的,而是一条链路

说得直白一点:没有封装,继承就没有意义——父类的字段都被外部直接改穿了,子类继承来的数据全是脏的;没有继承,多态也施展不开——因为多态最常见的实现方式就是"父类引用指向子类对象",没有继承关系这个引用就指不过去。

所以面试的时候如果只背"封装是隐藏细节、继承是复用、多态是同一个方法不同表现",你会发现面试官随便追问两句就卡住了。真正要理解的是它们之间怎么协作的。举个现实中的类比:一家公司里,封装是每个岗位只通过固定接口(邮件、工单)对外沟通,不把内部所有细节暴露出去;继承是管理层和基层员工都有人力资源管理这个公共流程,但各自有自己的权限范围;多态是"开绩效会"这个动作,在管理层那里是评审下属,在基层员工那里是汇报自己,同一个指令在不同角色身上有不同表现。这三者叠加,公司才能运转得清楚。

2. 封装:把复杂藏起来,把简单露出来

2.1 封装到底封的什么

很多 Java 教程讲到封装就一句话:把字段设为 private,提供 getter/setter。这话没错,但只讲对了一半。封装的核心目的不是"不让别人看",而是"保护内部状态的有效性"。

举个例子,你写一个订单金额字段,如果它是 public 的,外部代码可以直接order.amount = -100,这个负数的脏数据就这么混进系统里了。但如果我们把字段设为 private,然后提供一个 setter 方法,在方法内部加校验逻辑,比如"金额必须大于0",那所有外部赋值都要经过这道关卡。这时候封装保护的不只是数据本身,更是整个系统的业务规则。

所以真正理解封装,要看到两层含义:第一层是访问控制,用 private 等修饰符限制外部直接操作内部数据;第二层是行为封装,把校验、计算、状态变更的逻辑收敛到对象自身的方法里,外部只调用方法,不直接操作数据。这第二层,才是设计能力的体现。我在实际 review 代码时看过太多"类里全是 getter/setter,业务逻辑全部写在 Service 层"的写法,严格来说那只能叫"穿了马甲的数据结构",不叫面向对象设计。

2.2 Java 里实现封装的四个访问级别

Java 提供了四个访问修饰符,从松到严分别是 public、protected、默认(包访问权限)、private。这个顺序和具体语义必须记清楚,因为面试时经常直接考:

  • public:任何地方都能访问,最开放。
  • protected:同包任何类可访问,不同包只有子类可访问。
  • 默认(不写修饰符):只有同包的类可访问。
  • private:只有本类内部可访问。

这四个级别的含义看起来简单,但有一个细节很多人会错:protected 的成员,在不同包的子类里,只能通过"子类的对象"来访问,不能通过"父类类型的引用"来访问。换句话说,A 包里的子类继承 B 包的父类,在子类代码里可以用this.protectedField,但如果new一个父类对象并试图访问那个字段,编译器会报错。这个细节我在多次面试提问中看到候选人掉坑。

访问级别不是随便定的,要遵循一条从最小权限原则延伸出来的经验:字段一律先用 private,只把真正需要暴露给外部的方法设为 public。protected 用得相对少,主要出现在"父类给子类预留扩展点"的场景。包级别访问权限在大型项目里也很少见,一般只在同一包内协作的内部类之间使用。

2.3 实操示例:写一个带校验的订单金额类

光说不练假把式,我写一个常见的订单金额封装,尽量体现上面说的两层含义。

public class OrderAmount { private final long amountInCents; private OrderAmount(long amountInCents) { this.amountInCents = amountInCents; } public static OrderAmount of(double amount) { if (amount < 0) { throw new IllegalArgumentException("金额不能为负数"); } if (Double.isNaN(amount) || Double.isInfinite(amount)) { throw new IllegalArgumentException("金额不合法"); } long cents = Math.round(amount * 100); return new OrderAmount(cents); } public long toCents() { return amountInCents; } public OrderAmount add(OrderAmount other) { return new OrderAmount(this.amountInCents + other.amountInCents); } public OrderAmount multiply(int quantity) { if (quantity <= 0) { throw new IllegalArgumentException("数量必须大于0"); } return new OrderAmount(this.amountInCents * quantity); } @Override public String toString() { return String.format("%d.%02d", amountInCents / 100, amountInCents % 100); } }

这个类里,字段是 private final 的,构造器也私有,外部根本不能直接 new,只能通过静态工厂方法of创建。这样所有创建入口都走了校验逻辑,脏数据进不来。add和multiply这类操作也封装在类内部,返回值依然是 OrderAmount 对象,保证了后续计算仍然安全。

这种写法比裸的 getter/setter 强在哪?第一,金额用分存储,规避了浮点数精度问题;第二,非法状态在入口处就被拦截,而不是在系统深处爆雷;第三,所有算钱的动作都收口在类里,后续如果要加税费计算,直接在这一处改,全系统受益。这就是面向对象里聚合业务逻辑的价值。

2.4 封装设计里的常见糟点

经验告诉我,封装做得不好通常有三种典型症状。

第一种是 getter/setter 泛滥。一个类二十个字段,全配上 getter/setter,外部代码把它当数据容器随便改。这种类实际上没有任何业务语义,改起来风险完全失控。正确做法是仔细想想每个字段到底需不需要暴露,如果只是需要"只读",那就只给 getter,不给 setter。

第二种是内部可变状态直接暴露。常见表现是返回一个内部 List,外部拿到后直接add就能破坏对象内部数据。这不是访问修饰符能兜住的,需要在 getter 里做防御性复制,或者返回不可变视图,比如Collections.unmodifiableList。

第三种是过度封装。有的同学刚学完封装,把所有方法都设置成 private,外部想调什么都被堵死,最后 Service 层只能通过反射去调用——这就矫枉过正了。封装的目的不是把所有门都焊死,而是把该藏的内部实现细节藏起来,把该给外部用的接口开好。判断标准很简单:这个操作外部调用方真的需要自己控制吗?如果是基础设施级别的操作,开放出来反而更灵活。

3. 继承:复用代码还是复用设计

3.1 继承的本质与 Java 的单继承限制

继承的核心作用是抽取共性。多个类有相同的字段和方法时,把这些共性放到父类里,子类通过 extends 获得这些成员,这就是代码层面的复用。但继承的意义不只是在省几行代码,它更是一种"类型层面"的关系建模:Student 是 Person,Dog 是 Animal,子类可以安全地出现在所有需要父类的地方。

Java 设计成"单继承、多实现",一个类只能有一个直接父类,但可以实现多个接口。为什么不允许一个类继承多个父类?核心原因是菱形问题:假如 C 同时继承 A 和 B,而 A、B 都有同名方法 foo,那 C 继承的 foo 到底是哪个?编译器和运行时的规则会变得极其复杂。Java 选择单继承,牺牲了某些场景下的灵活性,换来了清晰且稳定的类型体系。

我经常用一句话向新人解释:继承适合描述"is-a"关系,不适合描述"has-a"关系。Student 是 Person,这是 is-a,用继承没问题;Student 有书包,这是 has-a,应该用组合(持有一个 Bag 对象)。很多设计腐烂的项目,就是到处用继承表达 has-a 关系,导致类层次越做越深,最后改一个父类崩一片子类。

3.2 super 关键字与构造器调用顺序

使用继承时,子类构造器必须直接或间接调用父类构造器。如果你在子类构造器里没有显式写super(...),编译器会自动插入一个无参的super()调用。这时候如果父类只有带参构造器而没有无参构造器,编译直接报错。这个考点几乎每次面试都会被问到。

更深一层的问题是:创建一个子类对象时,各成员的初始化顺序到底是怎么样的?我总结一个口诀级答案:

  1. 加载并初始化父类的静态成员和静态代码块,再初始化子类的静态成员和静态代码块。
  2. 进入子类构造器,但第一步是先调用父类构造器。
  3. 父类构造器内部先执行父类的实例成员初始化和实例代码块(按代码顺序),再执行父类构造器剩下的代码。
  4. 返回到子类构造器,先执行子类的实例成员初始化和实例代码块,再执行子类构造器剩下的代码。

这个顺序极其重要,因为很多隐蔽 bug 都源于此处。比如父类构造器里调用了一个可以被重写的方法,而子类重写了这个方法,那父类构造器执行到这里时,实际调用的是子类版本,但此时子类的字段还没初始化,拿到的是默认值。这就是经典的"构造器里调用可重写方法"陷阱。

3.3 方法重写:规则比写法更重要

方法重写(Override)是指子类定义一个与父类方法签名相同的方法,用@Override注解标注。重写的规则其实不多,但每一条都可能是面试题:

  • 方法名、参数列表必须完全一致。
  • 返回类型可以是父类返回类型的子类型(协变返回类型)。
  • 子类方法的访问修饰符不能比父类更严格。父类是 public,子类就不能改成 protected 或 private。
  • 子类方法不能抛出比父类更宽泛的受检异常。父类没抛 IOException,子类就绝不能抛 IOException。
  • static 方法不能被重写。子类里写一个和父类 static 方法同签名的方法,那叫隐藏(hide),调用时看引用类型,不参与运行时动态绑定。

为什么访问修饰符不能收紧,异常不能扩大?根本原因是里氏替换原则:凡是父类能出现的地方,子类对象也必须能安全地替换上去。父类对外承诺了 public 行为,子类要是把它收成 private,外部代码用父类引用调用时就可能崩;父类承诺不抛受检异常,子类要是抛了,调用方原本不用 try-catch,替换后就会编译失败。所以重写规则不是语法上故意为难人,它是在死守"子类必须能当父类用"这条底线。

3.4 继承的坑:什么时候不该用继承

虽然继承是 Java 的看家本领,但滥用继承的后果也非常惨烈。说几个我实际遇到过的场景。

场景一:为了复用两个方法而继承。比如有个 UserService,另一个 AdminService 觉得里面两个方法不错,直接 extends UserService。如果这两个方法在语义上根本不属于 Admin,那 AdminService 就被动地继承了它不该有的能力,未来父类改动还可能无意中改变管理员的业务逻辑。正确的做法是把公共方法抽到一个基础组件里,用组合持有。

场景二:继承层级过深。三层的类继承基本上就要开始警惕了,五六层的"类金字塔"必然导致理解困难和改动传染。往往改最底层一个字段的名字,上面十几处代码全要跟着动。这种情况下更合理的拆分是把继承树改成组合接口,把可变的维度用字段组合进去。

场景三:父类方法语义对子类不成立。典型的例子是"正方形继承长方形"。几何上正方形确实是一种长方形,但如果你在正方形子类里重写了 setWidth,让它同时把 height 也改了,那这块"正方形"放在任何期望"长方形"行为稳定的代码里都会引发诡异问题。这从实践层面说明:继承不是看"概念上是不是",而是看"行为上能不能完全兼容"。兼容不了,就别硬继承。

4. 多态:一个接口,多种实现

4.1 区分编译时多态与运行时多态

多态通常被分成两类:编译时多态和运行时多态。编译时多态是通过方法重载(Overload)实现的:同一个类里方法名相同、参数列表不同,编译器根据实参类型、数量、顺序在编译期决定该调哪一个。运行时多态是通过方法重写(Override)实现的:引用类型是父类,实际对象是子类,调用哪个方法在运行时根据实际对象类型决定。

这个区分很重要,因为很多人会把重载和重写搞混。重载是"同一个类里多个同名方法",重在参数差异;重写是"父子类之间同签名方法的不同实现",重在行为差异。面试时我一问到"重载和重写的区别",至少一半候选人会卡在"重载是编译期决定还是运行期决定"这个问题上。

要理解运行时多态的底层,就得知道动态绑定机制。Java 虚拟机在类加载阶段会给每个类建立一个方法表,表里存储了方法签名与具体实现代码的映射。当执行animal.sound()这类调用时,JVM 并不是简单地依据引用类型找方法,而是先拿到"实际对象的类型",再从该类型的方法表里查找匹配的签名,然后调用对应实现。这个查找发生在运行时,所以叫动态绑定。

4.2 向上转型与向下转型

多态的常见形态是向上转型:Animal a = new Dog();把一个 Dog 对象赋给 Animal 类型的引用。这之后,a只能调用 Animal 类型中定义的方法,不能直接调用 Dog 特有的方法,因为编译期编译器只看得到引用类型 Animal。

那么为什么要向上转型?因为在很多场景下,我们其实不关心具体子类,只需要把它当父类来统一处理。比如一个feed(Animal animal)方法,参数写成 Animal,就能接收所有动物的子类对象,方法内部调用animal.eat(),具体的 eat 逻辑由实际对象决定。如果参数不写成父类,而是分别写 Dog、Cat 各自的 feed 方法,那每加一种动物就要加一个方法,代码会迅速膨胀。

有向上就有向下。向下转型是把父类引用强制转回子类引用,比如Dog d = (Dog) a;。向下转型有风险:如果实际对象不是 Dog,运行时会抛 ClassCastException。所以规范的写法是先用instanceof判断类型,再转换。从 Java 16 开始,还有了 JEP 394 带来的模式匹配,if (obj instanceof Dog d)可以直接在判断类型的同时声明变量,省掉一次显式转型,代码更简洁安全。

4.3 接口与多态:面向接口编程

接口在 Java 里才是多态的最佳载体。类的继承受单继承限制,不能同时 extends 两个类,但可以实现多个接口。接口定义的是能力契约:一个类实现了某个接口,就是承诺它具备了这个接口里的能力。调用方只依赖接口,不依赖具体实现类,后续替换实现类或者增加新实现,调用方一行代码都不用改。

举个实际场景。你要做一个文件上传功能,可能本地磁盘存、OSS 存、自建 FTP 存。如果 Service 层直接依赖具体的 OssUploader 类,以后要切换存储就得改动 Service 层。但如果你定义一个FileUploader接口,里面只有一个upload(File file)方法,再让三个实现类分别实现它,Service 层只持有接口引用,通过构造器或者工厂在启动时注入具体实现,那换存储就只是改一行注入配置的事。

这个"依赖抽象、不依赖具体"的思想,正是面向对象设计里最核心的一条编码习惯。三大特性里,多态是最终的表现形式,而接口是让多态能够灵活扩展的基础。我在代码评审时有个习惯:看到两个类功能相似,但调用方各自 new 具体类、写了两套几乎一样的调用逻辑,就会建议定义一个接口,把相似调用统一收口。这既消减了重复代码,也为后续扩展留了门。

4.4 多态在框架源码里的影子

Java 生态里那些大牌框架,几乎全靠多态在运转。Spring 的 ApplicationContext 就是一个典型:你只把它声明为接口类型,具体是 ClassPathXmlApplicationContext 还是 AnnotationConfigApplicationContext,对业务代码完全透明。MyBatis 的 SqlSession 也是一样,接口定义语义,具体实现由框架运行时给你代理掉。JDBC 就更典型了,Connection、Statement全是接口,不同数据库厂商提供各自实现,你的业务代码写一次就能在 MySQL、Oracle、PostgreSQL 之间切换,不用改代码。这就是面向接口编程的力量。

理解这一点,对读源码帮助极大。很多初学者看 Spring 源码觉得类太多、绕来绕去,其实就是没理解多态。所有那些抽象类、接口、模板方法,本质都是在给"变化"开窗口:框架作者把固定的骨架定好,把容易变化的策略点抽象成接口,由使用者用不同实现去填充。你把这种"骨架固定、细节开放"的思路想通了,再去看源码里的各种*Template、*Strategy、*Handler,会有豁然开朗的感觉。

5. 三大特性的实战联动与设计原则

5.1 用一套支付体系演示联动

光讲概念不给整体案例,总感觉少点味道。我拿一个电商支付系统来串联三大特性,这也是面试里非常爱问的场景题。

首先定义支付这个动作的抽象。我用一个抽象类AbstractPayHandler定义公共骨架,再用一个接口Payable定义支付能力契约:

public interface Payable { boolean support(String channel); PayResult pay(PayRequest request); } public abstract class AbstractPayHandler implements Payable { @Override public PayResult pay(PayRequest request) { // 1. 通用参数校验(封装) validate(request); // 2. 记录开始时间 long start = System.currentTimeMillis(); // 3. 调用子类实现的支付逻辑(多态) PayResult result = doPay(request); // 4. 记录耗时和日志 log(request, result, System.currentTimeMillis() - start); return result; } protected abstract PayResult doPay(PayRequest request); private void validate(PayRequest request) { if (request == null || request.getAmount() == null) { throw new IllegalArgumentException("请求不能为空"); } } }

然后写多个实现类,例如AlipayHandler extends AbstractPayHandler、WechatPayHandler extends AbstractPayHandler。每个子类实现support和doPay,分别对接自己的渠道。调用方不管具体实现,只通过Payable接口调用。

这个例子里,封装体现在validate和日志逻辑被藏在抽象父类里,子类只关心自己的核心支付逻辑;继承体现在公共骨架抽取到父类,子类复用校验和日志流程;多态体现在运行时根据渠道选择不同的Payable实现,调用方却完全感知不到。三个特性在一个流程里环环相扣,这才是它们本来的协作方式。

5.2 三大特性如何支撑设计原则

设计模式背后的设计原则很多,但三大特性直接支撑的有两条最核心:开闭原则和里氏替换原则。

开闭原则说"对扩展开放,对修改关闭"。多态和接口是实现开闭原则的主要武器。继续拿上面支付系统说话:如果零花钱渠道兴起,要新增一个XxxPayHandler,你只需要新写一个类实现 Payable,然后在工厂类里注册一下渠道映射。已有的 AlipayHandler、WechatPayHandler 全部不用动,Service 层也不用动。这就是"加了新东西,却没改旧代码"。

里氏替换原则说"子类必须能替换父类且不破坏程序正确性"。这条原则是继承的紧箍咒。前面说的"正方形继承长方形",就是违背里氏替换的经典反例:一旦出现需要"长方形的 width 改了 height 不变"的场景,正方形替换进来就会出问题。所以做继承设计时,每写一个子类都问自己一句:如果我把父类引用全部换成这个子类,程序行为是不是依然稳定正确?如果答案有犹豫,那继承关系没建对。

5.3 设计模式里的三大特性

设计模式学习起来容易让人晕,但如果带着三大特性的眼光去看,很多模式都在围绕多态做文章。

策略模式是最典型的多态应用:把算法封装成一组实现同一接口的类,运行时按需选择。"支付方式选择"本身就是策略模式的现实案例。模板方法模式是继承应用的典范:父类定好算法骨架,子类填充可变步骤,上面写的 AbstractPayHandler 就是一个模板方法模式。工厂方法模式则是封装和多态的结合:创建对象的逻辑被封装到工厂里,工厂返回接口类型,调用方不感知具体产品类。观察者模式里,观察者就是通过接口注册和通知,解除事件源和具体监听方的直接依赖。

如果你学设计模式觉得抽象,建议把每个模式标注上"它主要用到了哪个特性、在哪个环节用到了多态",用这种方式将模式和基础特性绑定起来,会比死记硬背模式类图有效得多。

6. 面试高频痛点与避坑实录

6.1 重写和重载的终极对比清单

面试考多态必然绕不开重写和重载,我把最容易踩的坑整理成一个对比表:

对比项方法重载 Overload方法重写 Override
发生位置同一个类中父子类之间
方法签名参数列表必须不同参数列表必须完全相同
返回类型可以不同,无约束可以是父类返回的子类型
访问修饰符随意不能比父类更严格
静态方法可以重载不能重写,属于隐藏
决定时机编译期运行期
注解不需要建议加 @Override
私有方法可以重载不能重写,子类不可见

有个高频追问是:静态方法能不能参与重写?答案其实已经写在上表里:不能。静态方法属于类的行为,不跟随对象走。子类里写一个签名相同的 static 方法,本质是隐藏了父类的那个方法,调用时看引用类型。所以Person.getName()和Student.getName()虽然在代码上像重写,实际互不相干。面试时如果能主动说出"static 方法是隐藏不是重写,不参与动态绑定",会是很加分的一笔。

6.2 final 和 static 对三大特性的影响

final 在三大特性里的角色分为三块:final 类不能被继承,final 方法不能被重写,final 变量不能被重新赋值。三者里最容易踩坑的是 final 方法。有的开发者习惯把所有方法都标 final 防止被重写,这往往过犹不及——父类一旦把所有扩展点焊死,子类想做点增强就只能另起炉灶,很破坏扩展性。我的经验是:只有那些真正属于内部实现、不允许被篡改的方法才标 final,比如工具类的算法步骤、安全校验逻辑。

static 这个词也要看清楚。static 成员属于类而不属于对象,因此:static 方法没有多态特性,不能重写;static 字段可以被继承访问,但子类和父类实际上持有各自独立的同名字段,修改子类的 static 字段不会影响父类。平常写工具类,static 方法确实方便,但过度使用 static 方法往往意味着代码风格退回了"面向过程"模式——方法里满是一堆入参和局部逻辑,很少和对象状态协作,时间一长就成了面条代码。static 不是不能用,而是要在真正工具语义时才用。

6.3 构造器与代码块的执行顺序实测

前面讲 super 时我提过初始化顺序,现在给出一段可以实际跑起来的验证代码,方便你自己做实验:

class Parent { static { System.out.println("1. Parent 静态块"); } { System.out.println("3. Parent 实例块"); } Parent() { System.out.println("4. Parent 构造器"); } } class Child extends Parent { static { System.out.println("2. Child 静态块"); } { System.out.println("5. Child 实例块"); } Child() { System.out.println("6. Child 构造器"); } }

执行new Child()时,输出顺序是 1、2、3、4、5、6。这个顺序背后隐藏着一个高频面试点:如果父类构造器里调用了可重写方法,会触发怎样的陷阱?

我来构造一个真实的翻车现场:

class Parent { Parent() { init(); } void init() { System.out.println("Parent init"); } } class Child extends Parent { private String name = "child"; @Override void init() { System.out.println("Child init, name=" + name); } }

执行new Child()时,父类构造器先执行,调用init()进入动态绑定,实际走的是子类重写的 init 方法。但此刻子类的 name 字段还没执行初始化赋值,所以打印出来的是null。这就是为什么很多规范书里反复强调:不要在构造器里调用可以被重写的方法。如果父类构造器真的需要初始化某个依赖子类的条件,更安全的方案是把这个步骤设计成强制子类在构造器里显式传入参数,或者把逻辑推迟到子类构造器里执行。

6.4 实战中常见的继承封装错误清单

把这些年代码评审里反复出现的问题汇总一下,给有类似情况的项目提个醒:

  • 子类重写父类方法时缩小了访问权限。父类是 public,子类改成 protected,编译没问题,但调用方用父类引用调用就访问不到了,典型违反里氏替换。
  • getter 直接返回内部集合对象。外部拿到后一通 add,对象内部状态就被改了。正确姿势是防御性复制或返回不可变视图。
  • 继承层次过深导致方法来源不清。调用一个 method,得翻四五层父类才能找到实现,可读性极差,应该用组合替代深层继承。
  • 把所有字段都配上 getter/setter。看起来好像完成了封装,实际上等于没有封装,外部想怎么改就怎么改。要按业务需要暴露只读接口。
  • 重写方法和父类行为不一致。比如父类sort方法是稳定排序,子类重写后变成了不稳定排序,调用方基于父类行为做假设,结果被静默破坏。

这五条坑如果开发时能主动规避,代码质量会上升一个肉眼可见的档次。

7. 一点个人经验收尾

做 Java 这么多年,见了无数项目,我最大的体会是:封装、继承、多态从来不是面试题库里的死概念,它们写进代码里的方式,直接决定了一个项目是越维护越顺手,还是越维护越崩溃。

我自己现在写新类的时候会反复问自己三个问题:类的字段会不会被外部直接改脏?这类和那类之间到底是不是 is-a 关系?调用方依赖的应该是具体实现还是抽象接口?这三个问题想清楚了,代码自然就长成了健康的面向对象样子。

最后分享一个检查自己有没有理解三大特性的小技巧:如果你能用大白话向一个不懂 Java 的人解释清楚"封装是把细节藏起来、继承是让子类复用它爸爸的能力、多态是同一个按钮在不同人手里按出不同功能",并且能随手写出对应的代码示例,那这三大必考点就真的吃透了。如果还不能,就把文中那段构造器执行顺序的代码跑一遍,静下心看看输出,比硬背一百道面试题都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询