十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java多态从入门到精通:原理、实战与面试考点解析

Java多态从入门到精通:原理、实战与面试考点解析 当爹的引用指向儿子跑起来却是儿子的脾气——这句话我经常用来给刚入门的同事解释Java多态。多态作为面向对象三大特性封装、继承、多态中最难讲清楚的一个面试必问、工作必用但真正能把它讲透的人不多。很多初学者背完了父类引用指向子类对象这句话一写代码还是懵。这篇文章我结合自己多年用Java写业务系统的实际经验把多态从概念到原理、从应用到面试题一层层剥开看完你至少能应付大部分面试场景和日常开发中的设计问题。1. 多态到底解决什么问题1.1 先弄明白多态的含义多态Polymorphism字面意思就是多种形态。在Java里同一个引用类型变量调用同一个方法因为实际指向的对象不同表现出来的行为也不一样。用大白话说代码写成一样跑起来各不相同。这个特性解决的核心痛点是当系统里有一堆类型不同但行为相似的对象时如果每种类型都要单独写一套调用逻辑代码会膨胀到什么程度举个例子假设你要做一个动物园管理系统需要让猫吃食、让狗吃食、让熊猫吃食如果不用多态你得写几十个if判断每加一种动物就得改一次主逻辑。用多态的话所有动物都是动物统一调用吃食这个动作就行加新动物根本不用动主逻辑。1.2 从代码层面认识多态先看一段最基础的多态演示代码// 父类 class Animal { public void eat() { System.out.println(动物在吃东西); } } // 子类 class Cat extends Animal { Override public void eat() { System.out.println(猫在吃鱼); } public void catchMouse() { System.out.println(猫抓老鼠); } } // 子类 class Dog extends Animal { Override public void eat() { System.out.println(狗在啃骨头); } } // 测试类 public class TestPolymorphism { public static void main(String[] args) { Animal a1 new Cat(); Animal a2 new Dog(); feed(a1); // 输出: 猫在吃鱼 feed(a2); // 输出: 狗在啃骨头 } public static void feed(Animal animal) { animal.eat(); // 不管传进来的是什么动物统一调用eat方法 } }注意看feed方法它接收的是Animal类型但实际传入的是Cat和Dog。方法内部调用animal.eat()时Java运行时环境会判断当前实际对象是猫还是狗然后执行对应子类的eat方法。这就是多态最典型的应用形式面向父类编程调用子类实现。这段代码虽然简单但在业务系统里价值极大。后期如果加一个Bird类只要它继承了Animal并重写eat方法feed方法一行都不用改直接传进去就能跑。这种扩展能力靠的就是多态。1.3 为什么说多态是面向对象编程的灵魂封装解决了数据安全的问题继承解决了代码复用的问题但真正让系统具备扩展能力的是多态。没有多态继承就退化成单纯的代码复制工具继承体系的价值少了一大半。我把多态的价值归纳为三个字省、稳、活。省省代码、省维护成本。公共逻辑只写一遍各子类只需关注自己的差异部分。稳上层调用逻辑稳定。依赖抽象而非具体实现逻辑变化时不需要大面积修改调用方。活扩展灵活。新增功能时只需要增加新的子类不必改动旧的逻辑符合开闭原则。2. 多态的三大前提与底层运行原理2.1 多态必须满足的三个条件要形成多态光有继承关系还不够必须同时满足以下三个条件必须有继承或实现关系类继承类或类实现接口。必须有方法重写子类覆盖父类的方法或者实现类实现接口的方法。必须有父类引用指向子类对象也叫向上转型。三个条件缺一不可。很多人只记住了第三条忽略了前两条结果代码写出来方法一调用就露馅。尤其是第二条如果子类没有重写父类的方法那调用子类继承下来的那一份方法体所有子类行为都一样多态的效果就完全体现不出来。2.2 向上转型与向下转型多态里绕不开的两个概念是向上转型和向下转型。**向上转型Upcasting**是一个自动发生的动作子类对象可以自动转为父类引用不需要任何额外操作Animal animal new Cat(); // 自动向上转型向上转型是安全的因为子类必然具备父类的所有能力把子类当父类用能力只多不少任何时候都不会出问题。但它也有代价转型后父类引用只能调用父类中存在的方法无法调用子类特有的方法。比如上面的animal引用不能直接调catchMouse()因为编译器只看声明类型Animal而Animal里根本没有这个方法。**向下转型Downcasting**是逆操作把父类引用转回子类引用必须强制类型转换Animal animal new Cat(); if (animal instanceof Cat) { Cat cat (Cat) animal; // 向下转型 cat.catchMouse(); // 可以调子类特有方法了 }向下转型有风险。如果变量实际指向的是Dog对象强行转成Cat运行时会抛出ClassCastException。所以强制转换前一定要用instanceof判断这是安全操作的标准姿势。2.3 方法表、虚方法和动态分派关于原理Java面试中问得比较深的一个问题是多态在JVM中是怎么实现的答案是动态分派Dynamic Dispatch核心机制是方法表Method Table。JVM在加载类时会为每个类在方法区生成一张方法表。这张表里记录了这个类所有方法的实际入口地址。方法表的结构是一个数组每一项指向一个方法的具体实现。子类如果重写了父类的方法方法表中对应位置会直接指向子类自己的实现没有重写的方法才指向从父类继承来的实现。当代码调用animal.eat()时JVM不会在编译时确定调用哪个方法而是在运行时去查animal实际指向对象所属类的方法表找到eat()方法的入口地址然后调用。这就是动态分派。为了帮助理解可以想象一个点菜场景你去湘菜馆吃饭点一份辣子鸡去粤菜馆点同样的菜名厨师做出来的味道完全不同。菜单上写的是辣子鸡三个字但菜端上来是什么做法取决于你去的是哪家店。Java里也一样eat()就是菜名调用的实际逻辑取决于你创建的是哪个具体对象。2.4 编译看左边运行看右边这是整个多态里我总结最重要的一句话它解释了绝大多数让人困惑的现象编译时期编译器只看引用变量的声明类型左边。它能调用的方法集合由该类型决定。如果Animal没有catchMouse()方法那么animal.catchMouse()编译直接报错。运行时期JVM看实际创建的对象类型右边。方法具体执行哪份逻辑由实际对象的类决定。所以animal.eat()运行时会执行Cat重写后的eat()。牢记这句话再回头看多态的代码就不会迷茫了。3. 多态的典型应用场景与实践案例分析3.1 面向接口编程支付平台的经典姿势多态在真实项目中使用最广泛的场景就是面向接口编程。我拿一个电商系统的支付模块来做例子这个场景几乎所有Java开发者都遇到过。// 支付接口 public interface Payment { void pay(BigDecimal amount); } // 支付宝实现 public class Alipay implements Payment { Override public void pay(BigDecimal amount) { System.out.println(使用支付宝支付 amount 元); } } // 微信支付实现 public class WechatPay implements Payment { Override public void pay(BigDecimal amount) { System.out.println(使用微信支付 amount 元); } } // 银行卡支付实现 public class BankCardPay implements Payment { Override public void pay(BigDecimal amount) { System.out.println(使用银行卡支付 amount 元); } } // 支付服务类 public class PaymentService { private Payment payment; // 通过构造方法注入具体支付方式 public PaymentService(Payment payment) { this.payment payment; } public void checkout(BigDecimal amount) { // 统一调用支付方法高层的结算逻辑不用关心支付方式细节 payment.pay(amount); } } // 客户端使用 public class Application { public static void main(String[] args) { // 根据用户选择的支付方式创建不同的实现类 Payment payment new Alipay(); PaymentService service new PaymentService(payment); service.checkout(new BigDecimal(299.00)); } }这个例子至少有四个好处全部来自多态的功劳新增支付方式零成本比如以后新增云闪付只要写一个CloudQuickPay类实现Payment接口传入PaymentService就能用Service层一行不用改。业务逻辑解耦PaymentService只依赖Payment抽象不关心具体支付渠道。支付渠道如果换了签名算法、改了接口协议只要pay方法签名不变上层无感知。测试容易单元测试时可以用一个Mock实现甚至匿名内部类替换真实支付渠道无需真的去调用支付宝网关。可替换性强用户由支付宝切换成微信支付也只是换一个对象的事连方法调用都不用动。3.2 模板方法模式固定流程、变化细节多态配合继承可以实现模板方法模式。这在框架代码里极为常见。比如你开发一个数据导出器导出的整体步骤是固定的查数据、格式化、输出但每种格式的格式化细节不同public abstract class DataExporter { // 模板方法定义了算法骨架子类不能重写 public final void export() { ListString data fetchData(); String formatted formatData(data); writeToFile(formatted); } // 公共步骤查询数据也可以交给子类实现 protected ListString fetchData() { // 从数据库查数据... return Arrays.asList(数据1, 数据2); } // 抽象方法格式化数据必须由子类实现 protected abstract String formatData(ListString data); // 公共步骤输出数据 protected void writeToFile(String content) { System.out.println(写入文件 content); } } // CSV格式导出器 public class CsvExporter extends DataExporter { Override protected String formatData(ListString data) { return String.join(,, data); } } // JSON格式导出器 public class JsonExporter extends DataExporter { Override protected String formatData(ListString data) { return {\data\: data }; } } // 使用 DataExporter exporter new JsonExporter(); exporter.export(); // 流程固定最终输出的是JSON格式模板方法模式的前提就是多态export()方法中被调用的formatData()到底执行哪个版本由运行时对象决定。代码复用的同时把变化的部分留给子类各自实现这也是我在重构项目时非常喜欢用的一种设计。3.3 Java集合框架里的多态身影很多人每天在用List却没意识到里面全是多态ListString list1 new ArrayList(); ListString list2 new LinkedList(); ListString list3 new Vector();List是接口ArrayList、LinkedList、Vector是三个不同的实现类。代码里写的是List引用但运行时的增删改查行为各有不同这就是多态。好处是什么如果某天你发现ArrayList在频繁头插操作的场景下性能太差把new ArrayList()改成new LinkedList()就好其余代码一行不用动。再看Map、Set、线程池、各种框架中的Service、Mapper几乎所有能换实现的地方都在用多态。可以说你日常写的Java代码里多态无处不在只是很多初学者没有意识到。3.4 策略模式摆脱一长串if-else的利器多态最让人直呼真香的场景之一是解决代码里的一堆if-else分支。比如一个计算折扣的方法商品类型有普通、VIP、超级VIP三种不用多态写出来可能长这样public BigDecimal calculateDiscount(String userType, BigDecimal amount) { if (NORMAL.equals(userType)) { return amount; } else if (VIP.equals(userType)) { return amount.multiply(new BigDecimal(0.9)); } else if (SUPER_VIP.equals(userType)) { return amount.multiply(new BigDecimal(0.8)); } return amount; }每次加一种用户类型都要改这个方法时间长了方法变成几百行的大泥球。用策略模式改造之后// 折扣策略接口 public interface DiscountStrategy { String getUserType(); BigDecimal calculate(BigDecimal amount); } // 普通用户策略 public class NormalStrategy implements DiscountStrategy { Override public String getUserType() { return NORMAL; } Override public BigDecimal calculate(BigDecimal amount) { return amount; } } // VIP用户策略 public class VipStrategy implements DiscountStrategy { Override public String getUserType() { return VIP; } Override public BigDecimal calculate(BigDecimal amount) { return amount.multiply(new BigDecimal(0.9)); } } // 策略工厂通过Map收集所有策略消除if-else public class DiscountStrategyFactory { private static final MapString, DiscountStrategy STRATEGY_MAP new HashMap(); static { ListDiscountStrategy strategies loadAllStrategies(); for (DiscountStrategy strategy : strategies) { STRATEGY_MAP.put(strategy.getUserType(), strategy); } } // 用某种方式加载所有策略Spring可以直接注入 private static ListDiscountStrategy loadAllStrategies() { return Arrays.asList(new NormalStrategy(), new VipStrategy()); } public static DiscountStrategy getStrategy(String userType) { return STRATEGY_MAP.get(userType); } } // 使用 DiscountStrategy strategy DiscountStrategyFactory.getStrategy(userType); BigDecimal result strategy.calculate(amount);改造后加一种新用户类型就等于新增一个类原来的代码一行都不用动。而且每种策略内部逻辑内聚测试起来也更方便。策略模式的底层支撑就是多态 接口 集合容器。3.5 回调机制中的多态思想回调Callback本质上也是多态的应用。比如设置一个定时任务到了时间执行某个动作。Java里的TimerTask就是个抽象类你创建任务时通常会这么写Timer timer new Timer(); timer.schedule(new TimerTask() { Override public void run() { System.out.println(定时任务触发了); } }, 5000);这里通过匿名内部类创建了TimerTask的子类实例并重写了run方法。Timer底层在合适的时机调用run()时并不知道也不关心你要做什么只知道自己调用了TimerTask的run方法——具体执行什么逻辑由你传入的那个子类对象决定。从框架设计者的视角看这就是通过多态留出了一个扩展点让使用框架的人在不修改框架代码的前提下注入自己的逻辑。Spring里的各种Listener、Handler、Interceptor本质上也是这套思路。4. 多态VS重载VS重写别再把概念搞混4.1 三个容易混淆的术语面试问多态有几种表现形式时标准答案通常把多态划分为编译期多态和运行期多态编译期多态指方法重载Overload。同一个类里方法名相同、参数列表不同。到底调用哪个方法编译器在编译阶段根据参数类型就能确定所以叫编译期多态。运行期多态指方法重写Override 动态绑定。父类引用调用被子类重写的方法时具体执行哪个版本的逻辑由运行时对象类型决定。这两种多态虽然都带多态两个字但机制完全不同。国内很多教科书把重载也算作多态的一种但在实际的编码交流中程序员说的多态绝大多数时候指的都是运行期多态。4.2 重写和重载的细节对比对比维度方法重写Override方法重载Overload发生位置子类和父类之间同一个类中方法名必须相同必须相同参数列表必须相同必须不同类型、个数、顺序至少一项不同返回类型相同或是父类返回类型的子类型协变返回可以不同但不能仅凭返回类型区分访问修饰符不能比父类更严格比如父类是public子类不能是private无限制抛出异常不能抛出比父类更宽泛的受检异常无限制绑定方式运行时动态绑定运行期多态编译时静态绑定编译期多态注解建议使用Override让编译器帮忙校验通常没有专门注解重写有个很容易踩的坑返回类型可以不同但必须是被重写方法返回类型的子类型这叫协变返回类型。例如父类方法返回Animal子类重写时可以返回Cat因为Cat是Animal的子类。但如果返回String编译直接报错。4.3 重载决定调用只看编译类型重载有一个经典陷阱结合多态时会让人一脸懵。看这段代码class Parent { public void print(String content) { System.out.println(父类打印字符串 content); } } class Child extends Parent { public void print(Object content) { System.out.println(子类打印对象 content); } } public class TestOverload { public static void main(String[] args) { Parent p new Child(); p.print(hello); // 输出什么 } }很多初学者以为输出子类打印对象因为Child重载了一个print(Object)方法而String也是Object的子类好像子类的方法更适合。但实际输出是父类打印字符串hello。原因是p.print(...)这行代码选哪个重载版本编译时就根据引用类型决定了。p的声明类型是ParentParent里只有一个print(String)方法所以编译时选定调用它。运行阶段再根据实际对象类型执行动态分派——而Child没有重写print(String)所以执行的还是Parent的实现。Child中的print(Object)是新增的重载方法对p.print(hello)的编译期选择没有任何影响。这在面试中是一道区分度很好的题目理解了编译看左边运行看右边再结合重载看编译期类型重写看运行期类型基本就不会做错了。5. 多态实战3个高频坑每一个我都踩过5.1 坑一私有方法、静态方法和构造方法不参与多态Java里态不是对所有方法一视同仁的私有方法子类不可能重写访问不到的成员谈不上多态。子类里定义一个同名同参的方法顶多算新方法。静态方法静态方法属于类本身不属于对象实例。即使子类重写了父类的静态方法——实际上那叫隐藏Hide不是重写——调用时绑定到哪个方法也只看引用类型。看代码class A { public static void hello() { System.out.println(A.hello); } public void instanceHello() { System.out.println(A.instanceHello); } } class B extends A { public static void hello() { System.out.println(B.hello); } Override public void instanceHello() { System.out.println(B.instanceHello); } } public class TestStatic { public static void main(String[] args) { A a new B(); a.hello(); // 输出 A.hello静态方法看引用类型 A a2 new B(); a2.instanceHello(); // 输出 B.instanceHello实例方法看对象类型 } }上面的例子很直观地展示了静态方法和实例方法的多态性差异。要记住Java只针对实例方法提供运行时多态静态方法、私有方法都不参与。构造方法创建子类对象时会先调用父类构造方法但这不是多态调用。而且如果父类构造方法中调用了被子类重写的实例方法会触发子类的方法——这时候如果子类方法依赖子类尚未初始化的字段就会出现诡异的结果。这是一个隐藏的大坑后面会展开。5.2 坑二成员变量没有多态性多态只作用于实例方法成员变量字段不参与动态绑定。看这个例子class Parent { String name parent; public void printName() { System.out.println(name); } } class Child extends Parent { String name child; Override public void printName() { System.out.println(name); } } public class TestField { public static void main(String[] args) { Parent p new Child(); System.out.println(p.name); // 输出 parent字段看引用类型 p.printName(); // 输出 child方法看对象类型 } }p.name访问的是Parent.name因为字段的访问在编译期就确定了被静态绑定到引用类型上。而p.printName()因为被子类重写走的是Child的实现输出Child里的name字段。这种字段和方法的处理不一致让很多刚接触多态的人非常困惑。解决办法很简单把字段设为private通过getter/setter或方法去访问不要直接暴露字段。这样就不存在字段多态性的问题了也正好符合封装原则。5.3 坑三父类构造方法调用重写方法导致的提前泄漏这是多态问题里最隐蔽、也最容易在面试中被聊起来的一个经典案例class Parent { public Parent() { print(); // 构造方法中调用了可重写的实例方法 } public void print() { System.out.println(Parent.print()); } } class Child extends Parent { private String message hello; public Child() { super(); // 先调用父类构造方法 message world; } Override public void print() { System.out.println(Child.print(): message message); } } public class TestConstructor { public static void main(String[] args) { Child c new Child(); // 输出Child.print(): message null } }创建Child对象时JVM会先初始化父类部分也就是先调用Parent()构造方法。Parent()里调用print()由于动态绑定这个print()会执行Child重写后的版本但此刻Child的message字段还没有被赋值为hello字段初始化语句要等父类构造方法执行完才轮到所以message是null。这就是所谓的在构造方法中调用可重写方法陷阱。经验丰富的开发者写父类构造方法时会严肃对待每个被调用的方法确保它们不能被重写或者不依赖子类状态。如果确实需要父类钩子让子类参与初始化应该使用文章后面会讲的模板方法思想并严格规定钩子的执行时机和子类中允许的逻辑。所以在实际项目里父类构造方法中尽量避免调用任何可被重写的方法这是安全底线。如果必须要调用也要保证子类重写的方法能安全地处理字段为null的状态。5.4 强制类型转换的安全姿势前面提到向下转型可能抛ClassCastException这里给出规范的安全转换模板public void handler(Animal animal) { if (animal instanceof Cat) { Cat cat (Cat) animal; cat.catchMouse(); } else if (animal instanceof Dog) { Dog dog (Dog) animal; dog.guardHouse(); } else { // 兜底逻辑 } }有一点要留意instanceof在判断时会同时考虑继承关系和实际类型。比如cat instanceof Animal也返回true的因为Cat是Animal的子类。所以用instanceof判断时要先写子类、再写父类否则父类判断在前面就把逻辑短路了。另外从Java 14开始instanceof支持了模式匹配的写法可以简化代码if (animal instanceof Cat cat) { cat.catchMouse(); // 直接使用变量 cat }这种新写法比老式的判断 强转两步简洁不少我在新代码里已经习惯用了。5.5 被重写的equals方法对集合判断的影响多态会牵连到一些基础类的行为这里举一个常见的例子equals方法。如果你在子类中重写了equals但没有遵守反身性、对称性、传递性等契约用父类引用比较子类和父类对象时会出现判断不一致的问题。比如一个父类Person判断相等时比较id字段子类Employee在equals中又多比较了一个department字段。你用person.equals(employee)和employee.equals(person)两个方向去比较同一个对象对结果很可能不同从而违反对称性。很多集合类如HashSet、HashMap底层就依赖equals和hashCode一旦equals行为不对称可能出现对象在Set中却contains不到这种诡异的bug。所以重写带多态的equals时要慎之又慎。一个干净的方案是先判断类型是否相同再比较关键字段Override public boolean equals(Object obj) { if (this obj) return true; if (obj null || getClass() ! obj.getClass()) return false; Person person (Person) obj; return Objects.equals(id, person.id); }注意这里是用getClass() ! obj.getClass()而不是obj instanceof Person。前者严格判断类型一致可以避免父子类混在一起的对称性问题后者允许子类对象通过校验自由度更高但风险也更大。具体使用哪种取决于你的业务场景是否允许不同类型之间比较相等。我个人的原则是不允许跨类型相等比较时用getClass()精确判断。6. 面试高频题多态八股文的正确打开方式多态在Java面试中出现频率极高几乎算基础必考题。我把常考的几类问题整理了出来提供一个可以直接背、也可以扩展讲的回答思路。6.1 用一句话解释多态简洁版本多态是指同一类型的引用指向不同类型的对象时调用同一个方法会产生不同的行为。在Java中具体表现为父类引用指向子类对象运行时动态绑定到子类重写的方法上。如果面试官要求举例用两个代码片段说明即可先用AnimalCatDog这个最经典的例子再补充一个支付接口有支付宝、微信、银行卡实现的业务例子会显得既有基础功底又有实践经验。6.2 多态的实现原理是什么这个问题答到两个层面就算合格第一层概念层Java依赖三个条件——继承或实现关系、方法重写、父类引用指向子类对象。这三点满足后方法调用走动态分派JVM运行时根据实际对象类型找到对应的方法表入口执行子类的实现。第二层JVM层每个类在方法区有一张方法表表里记录了方法入口地址。子类重写父类方法时方法表中对应槽位的指向被子类的实现替换。虚拟机执行invokevirtual指令时会从操作数栈中取出对象的实际类型查找其方法表确定要执行的方法版本。这就是方法表 动态分派的核心机制。如果面试官追问那为什么方法的查找要查方法表会不会很慢你可以补充一个JIT优化点HotSpot虚拟机引入了内联缓存Inline Cache对于多态性不严重的调用点JIT会缓存最近的调用目标和对应的方法入口下次调用时直接命中缓存省去方法表查找的开销。这也是为什么现代JVM里多态调用的性能并不差不必为了性能放弃多态设计。6.3 重载和重写的区别这是固定的送分题按上面那张对比表回答就行。回答时建议补充一个小例子说明重载是编译期决定的重写是运行期决定的这样显得不是死记硬背。6.4 一个经典输出题用来检验你是不是真懂了面试官经常出下面这种输出题考察综合理解你可以拿来自测class A { public void show(A obj) { System.out.println(A.show(A)); } public void show(B obj) { System.out.println(A.show(B)); } public void show(C obj) { System.out.println(A.show(C)); } } class B extends A { Override public void show(A obj) { System.out.println(B.show(A)); } // 注意这是重载不是重写 public void show(B obj) { System.out.println(B.show(B)); } } class C extends B {} public class TestPolymorphism { public static void main(String[] args) { A a new B(); C c new C(); a.show(c); // 输出什么 } }这个问题的分析分两步选方法编译期a的声明类型是A实参c的静态类型是C。编译器在A类中找能匹配C类型参数的方法。show(A)、show(B)、show(C)中最接近C的是show(C)因为C直接就是C类型。所以编译期选中的是A.show(C)的方法槽位。定实现运行期a实际指向B对象。B类没有重写show(C)所以方法表中show(C)那个槽位仍指向A.show(C)的实现。最终输出A.show(C)。这类题目综合了重载的静态绑定选哪个方法和重写的动态绑定执行哪个实现两个阶段非常经典。掌握先选方法、再找实现的两步分析法大部分类似题目都能轻松拿下。6.5 为什么说多态符合开闭原则开闭原则Open-Closed Principle说的是对扩展开放对修改关闭。多态让系统可以在不修改现有代码的前提下通过新增类的方式扩展新的行为。还是用支付场景来说假如现有支付宝和微信支付两种实现新增云闪付时只需要新增一个实现Payment接口的类然后通过配置或工厂注入到系统中。支付服务层代码不用动已有的支付宝和微信支付也不用动新增功能不影响旧功能。这就是典型的扩展开放修改关闭。面试时如果能主动把这个设计层面的价值和多态联系起来会给面试官留下这个候选人写代码有设计意识的印象而不是停留在语法层面。7. 多态在实际开发中的设计建议多态用得好是左膀右臂用得不好就是满地坑。分享几个我写代码多年沉淀下来的实际建议。7.1 优先面向接口或抽象类编程而不是具体类日常写业务代码我给自己定了一条规则方法参数和返回值能声明成接口或抽象类就不声明成具体实现类。这一条对提升代码灵活性的帮助非常明显。如果一个方法的参数写成ArrayList调用方就只能传ArrayList如果写成ListArrayList、LinkedList都可以传。返回值的道理也一样。做API设计时把返回类型写成接口意味着未来你可以替换内部实现而不影响调用方这就是多态带来的契约式编程的好处。7.2 慎用向下转型能用多态解决就不要强转如果一个方法中频繁出现instanceof 强制转换说明多态的设计可能不到位。你还是在依赖具体类型而不是在依赖抽象。合理的重构是把那段根据不同类型做不同处理的逻辑通过一个接口方法抽出来让每个具体类自己实现自己的行为。当然向下转型并不是完全不可用比如某些场景下确实需要调用子类特有的能力比如Cat.catchMouse()但尽量把这种需求控制在局部、给足判空保护不要扩散到整个代码库。7.3 兄弟之间别乱转多态强调父类引用指向子类对象但是同父异母的兄弟类之间没有直接转换关系。Cat不能强转成Dog即使它们有同一个父类。原因很简单它们之间没有继承关系只是亲戚。任何从Cat到Dog的强制转换编译期就可以检测出错误。代码中如果出现这种转换需求通常说明设计模型本身有问题需要回头审视类的抽象层级是否合理。7.4 别在循环或热点路径里做无谓的instanceof判断很多人写代码有一个坏习惯在循环体内做一堆instanceof判断。这其实违反了多态的初心——你应该把差异逻辑封装到对象内部而不是在外面查户口。// 不推荐外部到处判断类型 for (Animal animal : animals) { if (animal instanceof Cat) { ((Cat) animal).catchMouse(); } else if (animal instanceof Dog) { ((Dog) animal).guardHouse(); } } // 推荐把行为交给对象自己 for (Animal animal : animals) { animal.doWhatItCanDo(); // 让每个子类自己实现 }第二种方案不仅更简洁而且扩展新动物时不需要改这个循环。这是依赖倒置原则的一个小实践高层模块不依赖低层模块的具体实现而是依赖抽象。行为细节由各对象自己通过多态完成。8. 从设计模式视角再看多态写到这里我想再多说一句多态不仅是个语法特性更是整个设计模式的基石。在Java生态里大量设计模式的实现都依赖多态明白这一点你学习设计模式时会有一种顿悟的感觉。策略模式依赖接口多态把算法族封装成一个个实现类。模板方法模式依赖继承多态把公共流程写在基类把变化步骤交给子类。工厂模式依赖接口多态工厂生产出的对象以接口或抽象类的形态交给调用方。观察者模式依赖接口多态观察者接口的不同实现类收到通知后做出不同的更新动作。状态模式依赖多态把状态机的逻辑拆分到各个状态类中消除复杂分支判断。这也是为什么我在带团队时要求初级工程师先吃透多态再去碰设计模式。多态理解不到位看设计模式就是雾里看花多态理解透了很多设计模式自己就能悟出来。从这个意义上讲多态不仅是一个面试知识点更是从能写代码到会设计代码的必经门槛。回顾我自己的经验多态这个东西理解概念只算入门真正上手还是要靠多写、多重构。我建议你找一个自己写过的、充满if-else的业务模块尝试用接口 多态的方式重构一遍感受一下改造前后的差异。等你亲手体会到新增一个策略只用加一个类那种轻快感才算真正把多态用到了实处。之后再去读框架源码比如Spring的BeanPostProcessor、MyBatis的Interceptor你会发现里面到处都是多态的影子那时候你对Java面向对象设计的理解就已经完全不一样了。
返回列表