
很多人在学习面向对象三大特性时都经历过这种状态课上听懂了封装的private、继承的extends、多态的重写考试也能及格但到了真正做项目时脑子里还是一团浆糊不知道该在什么场景下用哪个特性。我刚开始工作的头两年就是这样。直到后来维护了一个大型遗留系统每天被各种滥用继承的代码折磨又被几个设计得极为漂亮的多态模型惊艳过之后才逐渐想明白这三个特性的真正价值——它们是代码组织和演化的三条底层策略而不只是语法点。这篇文章我就以一个实际开发者的视角把封装、继承、多态从概念到实战整个讲透包括源码层面的机制、项目里最常见的用法、以及那些教科书上不会写但是工程里一定会踩的坑。1. 为什么说三大特性是代码组织与演化的底层策略如果我们抛开教科书上的定义单纯从代码演化的角度来审视这三个特性思路会清晰很多。一个软件项目的生命周期里需求一定在变代码一定在改。怎么让代码在不停修改的过程中保持清晰、稳定、可扩展答案就是利用这三大特性去控制代码之间的耦合方向。1.1 从代码演化的角度重新审视封装封装的核心逻辑不是“隐藏数据”这四个字而是控制变更的影响范围。假如没有封装所有字段直接暴露那么任何一行代码都能随意读取和修改任何字段。一旦业务规则发生变化比如用户名格式变了、余额不能为负数了所有被读写过的地方都要跟着改排查起来非常痛苦。有了封装之后字段一旦加上private外部代码就无法直接触达唯一能操作数据的路径变成了public方法。这意味着业务规则集中到了几个方法内部将来变更时只需要修改这几个方法内部逻辑调用方不需要改动。这里要特别注意封装真正的价值是提供了一个稳定的接口内部实现可以随时换掉。比如说我用一个类来表示一个文件配置内部可能是从JSON读取也可能是从数据库读取但对外暴露的loadConfig()方法保持不变调用方就感知不到底层变化。这就是封装的威力。1.2 继承解决的是“类型抽象”而不是“代码复用”很多人一提到继承第一反应就是“父类里有现成的代码子类可以直接拿来用”。这个理解本身没有错但会把继承引向一个非常危险的使用方向——为了省代码而去继承。继承本质上表达的是一种类型关系子类是一种父类。有了这层关系我们才能用父类引用去指向子类对象才能实现多态。比如Manager是一种EmployeeCircle是一种Shape这种“is-a”语义带来的类型抽象能力才是继承存在的核心意义。如果仅仅是为了复用代码优先考虑的应该是组合在一个类里持有另一个类的实例通过委托来复用功能。组合更加灵活不受单继承限制耦合也更低。后面我会用一个具体例子详细对比这两种方式。1.3 多态是实现“面向接口编程”的技术支撑多态的运作机制从程序员视角来看就是“同一个方法调用语句在实际运行时执行的是具体对象的那个版本”。这句话听起来简单但它背后隐藏了一个巨大的架构优势上层代码不必依赖具体实现类只依赖抽象类型就能驱动无限多个具体实现。这正是依赖倒置原则的实现基础。当你写了一个接受List参数的方法调用方可以传入ArrayList、LinkedList甚至任何List的实现你的方法完全不需要知道也不关心具体是哪个。系统因此变得更加开放——未来新增一个实现类已有的代码不用动扩展能力大幅提升。2. 封装的工程粒度从字段私有到模块自治聊完了底层逻辑我们进入实操层面。我在项目里看过大量封装不当的代码最常见的两种一种是所有字段干脆不设限直接public图省事另一种是每个字段都配上getter和setter看似封装了其实啥也没封住。这两类都背离了封装的本意。2.1 不只是private访问控制符的正确使用姿势Java提供的访问控制符有四种从宽到窄public、protected、默认包访问、private。很多人只会用public和private这是不够的。我分享一个实际场景。假设你开发一个内部工具包里面有个类专门处理字符串脱敏这个类放在com.company.utils包中。你希望包内的其他工具类能直接调用它的某个辅助方法但不希望外部项目的代码碰到它。这时把方法声明为默认访问控制不加任何修饰符就能实现“包内可见、包外不可见”的效果。类似地protected的语义是“子类可见但无关代码不可见”。这在设计框架、模板类时非常有用允许子类重写或访问某些受保护的方法但对外部调用者隐藏。下面用一个表格整理访问控制符的可见性范围访问控制符同类内包内子类跨包全局public是是是是protected是是是否默认无是是否否private是否否否实际开发中我的默认选择是字段一律private方法能收缩就收缩只有在明确需要扩大可见性时才逐步放宽。2.2 可变对象与不可变对象的封装差异封装时一个很容易忽略的细节是可变对象的安全暴露。比如public class Order { private ListString items; public ListString getItems() { return items; } }这个getter看起来标准实际上有一个大坑因为List是引用类型外部拿到items这个引用后可以直接调用add方法往里面加数据完全不经过Order类提供的任何校验逻辑。你的封装在引用传递面前被击穿了一半。正确的做法是返回一个不可变视图或者在不需要修改操作时返回副本public ListString getItems() { return Collections.unmodifiableList(items); }或者用Java 9的List.copyOf方法返回一个不可修改的副本public ListString getItems() { return List.copyOf(items); }这个细节在构建API、DTO、领域模型时尤其重要。很多时候我们以为用了private就算封装好了其实封装不仅仅是修饰符的问题还得考虑返回类型本身是否会让内部状态泄露。2.3 DTO、VO、Entity的分层本质是封装的放大版从更宏观的视角看分层架构中DTO数据传输对象、VO视图对象、Entity实体这些概念本质上是封装思想在类与类之间的延伸。Entity负责承载业务数据与规则不应该直接暴露给前端DTO用于跨进程传递数据把Entity的内部结构隔离起来。比如在一个Spring Boot项目中Controller层接收前端请求参数通常不能直接用Entity接收因为前端传参格式与数据库结构很可能不一致。我们要定义一个专门用于接收参数的DTO类把参数校验逻辑封装在其中再在Service层转换为Entity。这看似多写了几个类但实际上降低了层与层之间的耦合。某个字段Entity内部改了只要DTO接口没变Controller层体验不到反过来前端参数变了也只是改DTO和Controller的映射关系不影响底层数据结构。3. 继承的正确姿势与那些年我们踩过的坑继承是个好工具但也是最容易用错的语法。我维护的老系统里就嵌套了四五层继承关系改一个基类方法几十个子类的行为都可能受影响测试都要跑半天。下面我把继承的关键点和坑逐个讲清楚。3.1 构造器调用顺序子类初始化之前父类先初始化一个很基础但经常被忽略的知识点子类的构造器第一条语句一定是调用父类的构造器显式使用super(...)或隐式调用无参父类构造器。所以创建子类对象时对象的初始化顺序是父类静态块 → 子类静态块 → 父类实例变量初始化 → 父类构造器 → 子类实例变量初始化 → 子类构造器。这个顺序在项目里的实际意义是如果父类构造器调用了被子类重写的方法那么子类的实例字段可能还没有初始化完毕就会读到null或默认值。这是一个非常隐蔽的坑看个例子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); } }创建Child对象时输出的结果是“Child init, namenull”而不是预期中的“child”。原因是父类构造器执行时子类的字段初始化语句还没有跑完。虽然子类字段在字节码层面已经在准备阶段分配了内存但赋值在构造器链之后才开始。所以经验法则是在构造器中调用可被重写的方法是高危行为尽量避免如果确实需要优先调用非虚方法或static方法。3.2 super关键字的多层传递与菱形继承的破解super在Java中代表父类引用但多层继承之后super并不能直接跳到任意祖先层。例如GrandParent → Parent → Child在Child中使用super只能调用Parent的方法不能直接super到GrandParent的方法。如果需要必须在Parent中显式定义一个透传方法。至于C中著名的菱形继承问题一个类继承两个有共同祖先的类Java用“单继承类 多实现接口”的模型从语言层面消灭了。但接口也可以继承多个其他接口如果几个父接口里有相同签名的default方法子接口或实现类会强制要求自己重写并指定用谁的interface A { default void hello() { System.out.println(A); } } interface B { default void hello() { System.out.println(B); } } class C implements A, B { Override public void hello() { A.super.hello(); // 显式指定使用 A 的实现 } }这种处理方式确实麻烦但也强制开发者想清楚“我这个类到底应该怎么表现”而不是让编译器模棱两可。3.3 重写的规则不是你想改就能改子类重写父类方法时有几个硬性约束方法签名必须一致方法名、参数列表都相同。返回类型可以相同也可以是原返回类型的子类型协变返回类型。比如父类返回Object子类可以返回String。访问权限不能比父类更严格。比如父类是public子类就必须是public。不能抛出比父类更宽泛的受检异常。父类方法没声明抛IOException子类就不能抛IOException。日常开发中我强烈建议在重写方法上加上Override注解。这个注解不仅是给阅读者看的更重要的是编译器会帮你校验上面那些规则一旦不满足会直接报编译错误。如果一个方法你以为在重写实际因为参数类型写错变成了重载没有Override注解的话编译器是不会提示的运行时父类的逻辑就不会被替换掉——这类bug排查起来特别恼火。3.4 组合优先于继承的经典案例对比前面说到复用代码优先考虑组合这里用一个具体例子来对比。假设我们要给OrderService增加一个日志导出功能。继承方式可能这样写class OrderService extends FileExporter { // 继承了FileExporter的导出方法直接使用 }如果OrderService将来还需要导出到Excel呢A类无法同时继承两个类所以还得继续加父类层级或者改FileExporter的支持范围非常被动。组合方式则是class OrderService { private FileExporter exporter; public OrderService(FileExporter exporter) { this.exporter exporter; } public void export(Order order) { exporter.export(order); } }OrderService内部持有FileExporter并通过构造函数注入。将来要支持Excel只需要再实现一个ExcelExporter传入即可OrderService本身不用变化。这就是组合的最大优势行为在运行时可以自由切换而继承在编译期就把行为焊死了。所以我现在的编码准则很简单凡是“是”的关系用继承凡是“有”的关系用组合。Manager is an Employee用继承OrderService has a FileExporter用组合。4. 多态的实现机制与实战应用场景如果说封装和继承是代码内容的组织方式那么多态就是系统扩展性的灵魂。我先从JVM层面拆解一下多态到底是怎么做到的这能解答很多人心中“为什么运行时会走子类方法”的疑惑。4.1 虚方法表多态的底层真相Java中非静态、非private、非final方法默认就是虚方法调用时会走动态分派。JVM在类加载阶段为每个类维护一张虚方法表里面记录了类中所有虚方法的入口地址。子类继承了父类方法后如果重写了某个方法虚方法表中该方法的入口地址就会指向子类自己的实现如果没有重写则继续指向父类的实现。当程序执行到一个方法调用指令时JVM根据调用者对象的实际类型去对应的虚方法表中查找目标方法的入口地址。因为对象在创建时其内部的数据结构中已经包含了指向自身所属类的虚方法表引用所以运行时能精确找到正确的方法版本。这个过程也解释了为什么向上转型后调用被重写方法时会执行子类版本Animal animal new Dog(); animal.sound(); // 实际执行 Dog 覆盖后的 sound()animal变量的编译时类型是Animal但运行时实际对象是DogJVM会沿着Dog的虚方法表寻找sound()因此执行Dog的版本。而如果Dog没有重写sound()虚方法表里该条目仍然指向Animal的版本那就执行Animal的逻辑。4.2 重载与重写的本质区别多态分为编译时多态和运行时多态。重载是编译时多态发生在编译器确定调用哪个方法的阶段依据是方法名字相同但参数列表不同重写是运行时多态发生在JVM动态分发阶段依据是对象的实际类型。我遇到过一个有意思的笔试常考陷阱class Parent { public void print(String s) { System.out.println(Parent String); } } class Child extends Parent { public void print(Object o) { System.out.println(Child Object); } } public class Test { public static void main(String[] args) { Parent p new Child(); p.print(hello); } }这个输出结果是“Parent String”。原因是Child类中的print(Object)并没有重写Parent的print(String)参数列表不同是重载所以Parent的print(String)对Child来说仍然存在。调用p.print(hello)时编译阶段根据p的静态类型Parent确定选择signature为print(String)的方法于是走Parent的实现。虽然运行时对象是Child但Child没有重写print(String)虚方法表中该方法的入口仍然是Parent的版本。这类案例特别能检验开发者对“编译时绑定”和“运行时绑定”的理解程度我建议好好消化一下。4.3 各种语言多态的写法对比多态的实现方式在不同语言里有明显的差异看一眼对比能加深理解语言多态实现方式备注Java接口 继承 重写运行时分派基于虚方法表C虚函数 virtual关键字通过虚函数表vftable实现Python鸭子类型不要求继承关系只要对象有同名方法即可Dart继承 Mixinmixin可以混入多个行为对象Rusttrait 泛型静态分发 trait对象动态分发没有传统继承对比之后会发现静态语言大多数靠“继承体系 虚方法表”来完成动态分派而动态语言往往靠运行时类型检查来调用任何对象上的同名方法。Rust用trait对象完成类似多态的目标但采用显式对象安全约束保证调用成本预知。对于Java开发者来说理解虚方法表是理解多态的关键对于Python开发者来说理解鸭子类型意味着你甚至可以不需要继承只要对象实现了约定的行为即可灵活但缺少类型约束所以出现了协议Protocol来补齐。4.4 策略模式与模板方法多态在项目里最经典的两个落点模式是工具不是教条。但了解两个最常用的多态应用场景能让你的代码优雅许多。策略模式解决的是“同一功能的不同算法实现”问题。比如订单金额的折扣策略可能是满减、立减、VIP折扣。每个策略一个实现类公共接口定义统一的计算方法public interface DiscountStrategy { BigDecimal calculate(BigDecimal amount); } public class FullReductionStrategy implements DiscountStrategy { private BigDecimal threshold; private BigDecimal reduction; // 构造器、getter、setter 省略 Override public BigDecimal calculate(BigDecimal amount) { if (amount.compareTo(threshold) 0) { return amount.subtract(reduction); } return amount; } } public class VipDiscountStrategy implements DiscountStrategy { private double rate; // 构造器、getter、setter 省略 Override public BigDecimal calculate(BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(rate)); } }调用方OrderService只需要持有DiscountStrategy引用在运行时注入具体策略即可。新增折扣方案时新增一个实现类OrderService一行不用改这就是开放封闭原则的实践。模板方法模式则是利用继承来实现“算法骨架不变、具体步骤由子类实现”的需求。它的关键是把通用流程写在父类模板方法里把可变步骤抽成protected方法子类选择重写哪些步骤。比如一个数据导入器通用流程一定是打开文件 → 解析数据 → 校验数据 → 写入数据库。打开/解析/写入的逻辑在不同文件格式下是不同的校验逻辑可能部分相同。父类可以定义模板方法public abstract class AbstractDataImporter { public final void importData(String filePath) { Data data parseFile(filePath); validateData(data); writeToDatabase(data); } protected abstract Data parseFile(String filePath); protected void validateData(Data data) { // 默认校验逻辑非空、长度等 } protected abstract void writeToDatabase(Data data); }导入JSON、Excel各自的子类只需要关心自己怎么解析、怎么写入。校验逻辑作为钩子方法想要加强校验的子类就重写不需要变化的不动。我实际用模板方法重构过一个报表导出模块把从查询数据→填充模板→生成文件的流程统一到了父类子类只关心各种报表的数据填充逻辑代码量减少了将近一半。5. 一个贯穿案例支付模块重构中的三大特性协同设计理论讲了这么多真正能体现三大特性协作价值的往往是某个完整的业务模块。我挑一个非常经典的支付模块重构来串一遍因为支付扩展场景特别清晰多支付渠道几乎是标配需求。5.1 需求背景与最原始的糟糕版本项目起初只有支付宝支付当时的代码可能是这样的public class PaymentService { public void pay(String channel, BigDecimal amount) { if (alipay.equals(channel)) { // 调用支付宝SDK的支付逻辑 System.out.println(使用支付宝支付 amount); } else if (wechat.equals(channel)) { // 后来加微信支付直接在原方法里加分支 System.out.println(使用微信支付 amount); // 再后来加银联支付又来一个else if } } }这种写法的问题是显而易见的每次新增支付渠道都要修改pay方法相当于对支付金额逻辑的所有调用方都暴露了变异风险。而且随着分支增多方法越来越长各种支付渠道的不同参数、不同签名处理都堆在一个方法里迟早变成一锅粥。5.2 用封装、继承、多态重构后的设计重构分三步走正好用上三大特性。第一步封装支付参数。建立PaymentRequest类把原始金额、订单号、支付渠道类型、可选的附加参数都收拢到一个类中并提供校验逻辑public class PaymentRequest { private final String orderId; private final BigDecimal amount; private final PayChannel channel; private final MapString, String extraParams; public PaymentRequest(String orderId, BigDecimal amount, PayChannel channel, MapString, String extraParams) { if (orderId null || orderId.isBlank()) { throw new IllegalArgumentException(订单号不能为空); } if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(支付金额必须大于零); } this.orderId orderId; this.amount amount; this.channel channel; this.extraParams extraParams null ? Map.of() : Map.copyOf(extraParams); } public String getOrderId() { return orderId; } public BigDecimal getAmount() { return amount; } public PayChannel getChannel() { return channel; } public MapString, String getExtraParams() { return extraParams; } }这里字段设为final构造器内完成校验extraParams通过Map.copyOf防止外部修改内部集合。这就是封装的完整落地。第二步用继承建立支付渠道的抽象体系。定义一个抽象父类AbstractPayChannel包含所有渠道都需要做的通用逻辑记录日志、处理重复通知等同时定义两个抽象方法要求子类实现各自的签名和下单逻辑public abstract class AbstractPayChannel { protected abstract String sign(MapString, String params); protected abstract boolean requestPayment(PaymentRequest request); public final boolean pay(PaymentRequest request) { // 1. 统一埋点日志 System.out.println(开始支付订单号 request.getOrderId() 金额 request.getAmount()); // 2. 渠道特有签名 String sign sign(request.getExtraParams()); if (sign null || sign.isBlank()) { return false; } // 3. 每个渠道自己的下单请求 return requestPayment(request); } }这里pay被声明为final子类不需要也不应该重写整个算法流程只需要关注签名和请求这两个变化点。第三步各种支付渠道的类继承父类填充各自实现public class AlipayChannel extends AbstractPayChannel { Override protected String sign(MapString, String params) { // 支付宝的RSA签名逻辑 return alipay-sign; } Override protected boolean requestPayment(PaymentRequest request) { // 调用支付宝SDK下单 System.out.println(调用支付宝下单接口); return true; } } public class WechatPayChannel extends AbstractPayChannel { Override protected String sign(MapString, String params) { // 微信的MD5签名逻辑 return wechat-sign; } Override protected boolean requestPayment(PaymentRequest request) { // 调用微信支付下单接口 System.out.println(调用微信支付下单接口); return true; } }最后在PaymentService里维护一个Mapkey是渠道枚举value是通道实现类public class PaymentService { private final MapPayChannel, AbstractPayChannel channelMap; public PaymentService() { channelMap new HashMap(); channelMap.put(PayChannel.ALIPAY, new AlipayChannel()); channelMap.put(PayChannel.WECHAT, new WechatPayChannel()); } public boolean pay(PaymentRequest request) { AbstractPayChannel channel channelMap.get(request.getChannel()); if (channel null) { throw new UnsupportedOperationException(不支持的支付渠道 request.getChannel()); } return channel.pay(request); } }此时pay方法已经没有if-else了完全基于多态完成分发。将来要接入银联只需要新增一个UnionPayChannel类在初始化的map里加一行业务调用方的代码一行都不用动。5.3 重构带来的具体收益评价这个重构价值不是体现在语法上的“用了三大特性”而是体现在几个可量化的维度上新增支付渠道的代码改动量从原来的“改方法体加分支”变为“新增类 注册一行”各渠道之间彻底隔离不会出现支付宝改坏微信的情况测试也可以针对具体Channel做单元测试不再需要构造全路径分支覆盖。我在另外的项目中也见过类似用switch分发支付渠道的写法逻辑稍复杂一点就出现渠道参数泄漏、幂等处理重复、日志混乱等问题。与其等代码腐化了再重构不如在一开始就按多态模型设计。6. 面试与笔试题里关于三大特性的高频误区把三大特性学扎实能直接体现在面试表现上。下面这几个点是我经常看到候选人和笔试者搞混的单独拎出来列一下。6.1 静态方法为什么不能重写静态方法属于类本身不属于对象实例。所以子类定义一个与父类静态方法签名相同的方法它不是重写而是“隐藏”。调用哪个方法取决于你的引用类型class Parent { public static void show() { System.out.println(Parent static show); } } class Child extends Parent { public static void show() { System.out.println(Child static show); } } Parent p new Child(); p.show(); // 输出 Parent static show Child c new Child(); c.show(); // 输出 Child static show由于静态方法调用在编译期就决定了所以没有运行时多态。这也是为什么我建议不要通过对象引用来调用静态方法直接用类名调用能避免歧义。6.2 向上转型后能访问哪些成员父类引用指向子类对象时只能访问父类中定义的成员包括被重写后仍由子类实现的虚方法无法访问子类新增的成员。比如class Parent { public void parentMethod() {} } class Child extends Parent { public void childMethod() {} } Parent p new Child(); p.parentMethod(); // 没问题 p.childMethod(); // 编译错误Parent类型中没有childMethod这一点在设计接口时很有用尽量把调用方需要的方法定义在父类或接口中否则上层拿不到向下转型的资格就在抛ClassCastException的边缘试探了。6.3 多态与类型转换的常见陷阱向下转型强转不是随便用的前提是对象确实是目标类型。错误的强转会在运行时抛ClassCastException。更稳妥的写法是用instanceof判断或者用Java 16的instanceof模式匹配if (animal instanceof Dog dog) { dog.fetch(); }这个新特性不仅简化了代码还自动帮你完成了强转很舒服。我见到的另一个陷阱是集合泛型缺乏多态性。List 并不是List 的子类型因此不能把一个List 直接赋给List 。泛型默认是不变的invariance想实现协变需要使用通配符ListDog dogs new ArrayList(); List? extends Animal animals dogs; // 正确协变但是通过这个通配符声明的List只能读不能添加新元素因为编译器不确定里面的具体类型。这也是面试里爱问的“泛型为什么不支持多态”本质是类型安全权衡的结果。7. 日常开发中的设计建议与个人经验7.1 一定不要为了设计而设计三大特性是工具不是终极目标。如果一个类未来几乎不可能有第二个实现硬加接口去“面向接口编程”反而增加理解成本。我见过一个demo项目恨不得给每个service都配接口两个实现类最后维护的人根本分不清该用哪个。正确的判断标准是看变化的方向。如果你确实能看到未来的扩展点比如支付渠道、消息推送渠道、多种存储方案那么现在就抽象出一个稳定的接口让依赖这个接口的代码不跟随实现变化。如果只是CRUD一个用户表真的没必要套一堆设计模式进去。7.2 package-info.java也是封装边界在我工作的Java项目中还会利用package-info.java编写包级别的文档标注这个包对外暴露的入口类、内部约定。这虽不是语法层面的强约束但在团队协作里能帮助其他人理解包设计的边界不会轻易在一个工具包里随意加public类。另外模块化Java 9的module-info.java更进一步可以对外隐藏包内实现只开放指定的导出包。这算是封装思想在模块层面的终极形态。7.3 测试驱动你更好地理解继承与多态写单元测试能帮你发现继承设计的问题。如果你为了测试某个类不得不mock掉很多父类行为说明父类对子类的侵入太强了。一个好的多态设计里各个实现类应该像插件一样可以独立替换、独立测试。我在重构支付模块时给每个Channel都单独写了测试类互相之间完全隔离测试稳定性大幅提升。反过来如果某个实现类测试里频繁依赖另一个类的内部数据就要检查封装是否被破坏了。7.4 学三大特性最有效的方式读优秀源码框架源码是学习封装、继承、多态的最佳教科书。比如Spring的BeanFactory体系顶层是接口中间有抽象类实现一些通用逻辑底层有各种具体容器。你在看它的类图时能直观地看到抽象、扩展、分工是如何安排的。再比如Java集合框架的Collection接口 → AbstractList抽象类 → ArrayList实现类这套三层结构就是把稳定接口、公共逻辑、具体实现分开的范本。建议大家挑一个自己常用的框架把核心类之间的继承关系画出来远比背十遍“什么是多态”有效。我入行前几年踩过不少继承的坑后来靠大量读源码、反复重构小案例才慢慢形成自己的设计手感。希望这篇文章能帮你少走一点弯路把封装、继承、多态真正变成你写代码时自然流露的能力。