尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入理解面向对象编程多态性:从原理到实战应用

深入理解面向对象编程多态性:从原理到实战应用 1. 多态性面向对象编程的灵魂与艺术如果你写过几行面向对象的代码无论是Java、Python还是C大概率都听过“多态”这个词。它常常和“封装”、“继承”一起被奉为面向对象编程的三大基石。但说实话我第一次接触这个概念时感觉它就像个玄学词汇——听起来很厉害但具体怎么用、为什么用总有点云里雾里。直到后来在项目里踩了几个大坑重构了几次屎山代码才真正体会到多态性那种“四两拨千斤”的魔力。它不是一句简单的“一个接口多种实现”就能概括的而是设计出灵活、可扩展、易维护代码的核心思想。今天我们就抛开那些教科书式的定义从一个一线开发者的视角聊聊多态到底是什么以及它如何在实际编码中拯救你的项目。简单来说多态性让你能用一种统一的方式去操作不同类型的对象而具体执行哪个对象的行为则由对象自身的类型在运行时决定。这听起来有点抽象我举个生活中的例子想象一下“支付”这个动作。无论是用微信支付、支付宝、信用卡还是现金对你调用者而言你只关心“完成支付”这个结果。至于背后是扫码、输入密码还是掏钞票那是每种支付方式具体对象自己的事。多态性让你可以定义一个“支付”接口然后让不同的支付方式去实现它。当你在收银台说“我要支付”时收银系统不需要写一堆if-else来判断你用什么支付它只管调用“支付”方法具体怎么付由你手里的“支付工具”自己决定。这就是多态带来的最直接好处代码的调用方和实现方解耦了系统变得更容易扩展和维护。这篇文章适合所有正在或即将使用面向对象语言的开发者无论你是刚入门的新手还是想重新梳理概念的老手。我们会从为什么需要多态开始拆解它的两种核心类型编译时和运行时然后深入到实现机制如虚函数表最后通过大量实际场景和避坑指南让你不仅理解概念更能掌握如何用好它。你会发现用好多态你的代码会从“能跑就行”进化到“优雅健壮”。1. 为什么我们需要多态性从“if-else地狱”说起在深入技术细节前我们先看看没有多态的世界是怎样的。假设我们要开发一个简单的图形编辑器可以绘制圆形、矩形和三角形并计算它们的面积。1.1 面向过程思维的局限冗长的条件判断如果没有多态我们很可能会写出下面这样的代码以Java风格伪代码为例// 定义不同的图形类 class Circle { double radius; } class Rectangle { double width, height; } class Triangle { double base, height; } // 一个处理所有图形的工具类 class GraphicsProcessor { void draw(Object shape) { if (shape instanceof Circle) { Circle c (Circle) shape; // 绘制圆形的具体代码... System.out.println(Drawing a circle with radius: c.radius); } else if (shape instanceof Rectangle) { Rectangle r (Rectangle) shape; // 绘制矩形的具体代码... System.out.println(Drawing a rectangle.); } else if (shape instanceof Triangle) { Triangle t (Triangle) shape; // 绘制三角形的具体代码... System.out.println(Drawing a triangle.); } else { throw new IllegalArgumentException(Unsupported shape type); } } double calculateArea(Object shape) { if (shape instanceof Circle) { Circle c (Circle) shape; return Math.PI * c.radius * c.radius; } else if (shape instanceof Rectangle) { Rectangle r (Rectangle) shape; return r.width * r.height; } else if (shape instanceof Triangle) { Triangle t (Triangle) shape; return 0.5 * t.base * t.height; } else { throw new IllegalArgumentException(Unsupported shape type); } } }这段代码能工作但它存在几个致命问题违反开闭原则每当要新增一种图形比如六边形你必须修改GraphicsProcessor类中的每一个方法添加新的if-else分支。这就像在一个已经封装好的盒子上硬生生开个洞破坏了代码的封闭性。代码高度耦合处理器必须知道所有具体图形类型的存在并与它们紧密绑定。这导致处理器类异常庞大且难以维护。容易出错大量的类型检查和强制转换instanceof和(Circle)shape不仅让代码丑陋还增加了运行时ClassCastException的风险。重复代码类型判断的逻辑在每个方法里都要重复写一遍。这种代码结构在小型项目或脚本中或许能忍受但在中型以上项目中它会迅速演变成难以维护的“屎山”。多态性正是为了解决这些问题而生的。1.2 多态性带来的范式转变将“做什么”与“怎么做”分离多态性的核心思想是抽象和委托。我们不再让一个中央控制器去判断该做什么而是定义好一套行为契约接口或抽象类让每个具体的对象自己来决定如何履行这个契约。让我们用多态重构上面的图形编辑器// 1. 定义一个抽象的“图形”契约 abstract class Shape { abstract void draw(); // 抽象方法声明“可以绘制” abstract double calculateArea(); // 抽象方法声明“可以计算面积” } // 2. 具体图形类实现这个契约 class Circle extends Shape { double radius; Circle(double radius) { this.radius radius; } Override void draw() { // 圆形自己负责如何绘制自己 System.out.println(Drawing a circle with radius: radius); } Override double calculateArea() { // 圆形自己知道面积公式 return Math.PI * radius * radius; } } class Rectangle extends Shape { double width, height; Rectangle(double w, double h) { width w; height h; } Override void draw() { System.out.println(Drawing a rectangle.); } Override double calculateArea() { return width * height; } } // 3. 图形处理器变得极其简单和稳定 class GraphicsProcessor { void drawShape(Shape shape) { // 参数是抽象类型Shape shape.draw(); // 直接调用具体画什么由shape决定 } double getArea(Shape shape) { return shape.calculateArea(); // 直接调用具体怎么算由shape决定 } } // 4. 使用示例 public class Main { public static void main(String[] args) { GraphicsProcessor processor new GraphicsProcessor(); Shape circle new Circle(5.0); Shape rectangle new Rectangle(4.0, 6.0); processor.drawShape(circle); // 输出: Drawing a circle with radius: 5.0 processor.drawShape(rectangle); // 输出: Drawing a rectangle. System.out.println(Circle area: processor.getArea(circle)); System.out.println(Rectangle area: processor.getArea(rectangle)); } }看到区别了吗GraphicsProcessor的代码变得极其简洁和稳定。它只依赖抽象的Shape类型完全不知道Circle或Rectangle的存在。它只是发出指令“你画一下自己”或“你算一下面积”。至于怎么画、怎么算那是每个图形对象自己的责任。这种设计的威力在于扩展性。现在如果你需要添加一个Triangle三角形你只需要做一件事创建一个继承自Shape的Triangle类并实现draw()和calculateArea()方法。你完全不需要修改GraphicsProcessor类哪怕一行代码。GraphicsProcessor可以无缝地处理这个新的图形类型。这就是“对扩展开放对修改关闭”开闭原则的完美体现。实操心得识别多态的应用场景当你发现代码中出现以下“坏味道”时就是考虑引入多态性的好时机冗长的if-else或switch-case语句它们根据对象类型执行不同的行为。多个类有相似或相关的方法但实现不同。你预感到未来可能会有新的类型加入并且不希望频繁修改现有核心逻辑。你想降低模块间的耦合度让高层模块不依赖于低层模块的具体实现而依赖于抽象。2. 多态性的两种核心类型编译时与运行时多态性并非只有一种形式。根据绑定行为发生的时间点我们可以将其分为编译时多态静态多态和运行时多态动态多态。理解两者的区别对于编写高效且正确的代码至关重要。2.1 编译时多态方法重载编译时多态主要指方法重载。它发生在编译阶段编译器根据方法调用时传递的参数类型、个数和顺序来决定具体执行哪个方法。public class Calculator { // 重载add方法 public int add(int a, int b) { return a b; } public double add(double a, double b) { return a b; } public int add(int a, int b, int c) { return a b c; } public static void main(String[] args) { Calculator calc new Calculator(); System.out.println(calc.add(1, 2)); // 调用 add(int, int) System.out.println(calc.add(1.5, 2.5)); // 调用 add(double, double) System.out.println(calc.add(1, 2, 3)); // 调用 add(int, int, int) } }核心特点与原理绑定时机编译时。在生成字节码文件.class时编译器就已经确定了calc.add(1,2)这条语句具体调用的是哪个方法。这被称为“早期绑定”或“静态绑定”。判断依据方法签名。方法签名由方法名和参数列表参数类型、个数、顺序共同决定。返回值类型不同不构成重载。发生范围通常在同一个类内部。性能由于在编译期就确定了调用目标没有额外的运行时查找开销因此性能最好。注意事项重载与返回值无关仅改变方法的返回值类型不改变参数列表这不是重载会导致编译错误。因为调用时无法区分你希望调用哪个方法例如int func()和double func()调用func()时编译器不知道你想要哪个返回值。自动类型转换的影响编译器会选择“最匹配”的方法。例如调用calc.add(1, 2.0)由于1是int2.0是double没有完全匹配的add(int, double)编译器会尝试类型提升可能调用add(double, double)。这有时会导致意想不到的结果需要小心。2.2 运行时多态方法重写与接口实现运行时多态是面向对象中更核心、更强大的多态形式通常通过方法重写继承和实现接口来实现。它允许程序在运行时根据对象的实际类型来决定调用哪个方法。// 基类 class Animal { public void makeSound() { System.out.println(Some generic animal sound); } } // 派生类 class Dog extends Animal { Override // 注解表明这是重写非必须但推荐 public void makeSound() { System.out.println(Woof! Woof!); } } class Cat extends Animal { Override public void makeSound() { System.out.println(Meow!); } } public class TestPolymorphism { public static void main(String[] args) { // 编译时类型是Animal运行时类型是Dog Animal myAnimal new Dog(); myAnimal.makeSound(); // 输出: Woof! Woof! myAnimal new Cat(); myAnimal.makeSound(); // 输出: Meow! // 即使放在数组里统一处理也能表现出各自的行为 Animal[] animals {new Dog(), new Cat(), new Animal()}; for (Animal a : animals) { a.makeSound(); // 依次输出: Woof! Woof! / Meow! / Some generic animal sound } } }核心特点与原理绑定时机运行时。在程序运行期间JVM根据对象在堆内存中创建的实际类型new关键字后面跟的类来决定调用哪个方法。这被称为“晚期绑定”或“动态绑定”。判断依据对象的实际类型。变量myAnimal的声明类型编译时类型是Animal但它引用的是一个Dog对象运行时类型。调用makeSound()时JVM会找到这个Dog对象并执行其重写后的方法。发生条件继承关系或接口实现必须有父类或接口和子类。方法重写子类提供了与父类方法签名完全相同的方法。向上转型通过父类引用指向子类对象Animal a new Dog()。性能由于需要在运行时查找方法的具体实现通常通过虚方法表会有轻微的性能开销但现代JVM的优化如JIT编译、内联缓存使得这个开销在绝大多数场景下可以忽略不计。其带来的设计灵活性的收益远大于此开销。两种多态的对比特性编译时多态 (方法重载)运行时多态 (方法重写)绑定时机编译时运行时判断依据方法签名参数列表对象的实际类型典型实现同一个类中的多个同名方法子类重写父类方法或类实现接口方法关系通常在同一类内存在于有继承/实现关系的类之间关键字无特定要求extends,implements,OverrideJava设计目标提供同一功能的不同使用方式参数不同实现同一接口的不同行为支持扩展和替换避坑指南重写 vs 重载新手最容易混淆这两个概念。记住一个简单的区别重写是“垂直”关系父子类方法签名相同改变的是行为实现重载是“水平”关系同类内方法签名不同提供的是功能变体。误用会导致逻辑错误。例如如果你本意是重写父类方法却写错了参数列表编译器会认为这是一个新的重载方法不会报错但运行时不会调用到你期望的方法导致bug非常隐蔽。3. 运行时多态的底层机制虚方法表理解了运行时多态的表现我们不禁要问JVM是怎么在运行时找到正确的方法的这就引出了底层的一个关键数据结构——虚方法表。3.1 什么是虚方法表虚方法表是Java、C等支持动态绑定的语言中用于实现运行时多态的一种常见机制。每个类在加载到内存后都会有一个与之关联的虚方法表。这张表可以理解为一个“方法指针数组”其中存储了该类所有虚方法在Java中非private、非static、非final的实例方法默认就是虚方法在内存中的实际入口地址。工作原理简述当通过父类引用调用一个方法时如animal.makeSound()JVM不会直接去父类的方法定义里找地址。它会先找到对象在堆中的实际类型比如Dog。然后访问Dog类的虚方法表在表中查找makeSound方法对应的条目。最后根据条目中存储的地址跳转到Dog.makeSound()方法的代码处执行。3.2 通过示例理解虚方法表查找让我们用之前的Animal-Dog-Cat例子来模拟一下。假设内存中的结构简化如下Animal类虚方法表[makeSound - Animal.makeSound()的地址]Dog类虚方法表[makeSound - Dog.makeSound()的地址](重写了)Cat类虚方法表[makeSound - Cat.makeSound()的地址](重写了)执行流程Animal a new Dog(); // a引用指向一个Dog对象 a.makeSound();JVM发现变量a的声明类型是Animal要调用makeSound。它找到a实际引用的对象发现其实际类型是Dog。它去查找Dog类的虚方法表。在表中找到makeSound对应的条目里面是Dog.makeSound()的地址。执行Dog.makeSound()输出“Woof! Woof!”。如果换成Animal a new Animal();那么查找的就是Animal类的虚方法表执行的就是Animal.makeSound()。3.3final、static和private方法的影响虚方法表只对“虚方法”有效。以下方法不参与动态绑定因此不会出现在虚方法表中或者调用方式不同final方法final方法不能被重写因此它的实现在编译期就是确定的。JVM可以对其进行优化如内联调用时无需查表直接绑定。这既保证了安全又提升了性能。static方法静态方法属于类不属于任何对象实例。调用静态方法使用的是“类名.方法名”的形式在编译期就完成了绑定静态绑定。它不存在多态性。即使通过对象引用调用静态方法实际调用的也是引用类型所属类的静态方法与对象实际类型无关。这是一个常见的陷阱。private方法私有方法隐式指定为final且对子类不可见因此不能被重写。调用私有方法也是静态绑定。class Parent { public static void staticMethod() { System.out.println(Parent static); } private void privateMethod() { System.out.println(Parent private); } public final void finalMethod() { System.out.println(Parent final); } public void normalMethod() { System.out.println(Parent normal); } } class Child extends Parent { // 这不是重写只是隐藏了父类的静态方法 public static void staticMethod() { System.out.println(Child static); } // 这不是重写父类的private方法而是一个全新的方法 private void privateMethod() { System.out.println(Child private); } // final方法不能重写 // public void finalMethod() { } // 编译错误 Override public void normalMethod() { System.out.println(Child normal); } } public class TestBinding { public static void main(String[] args) { Parent p new Child(); p.staticMethod(); // 输出: Parent static (静态绑定看引用类型Parent) // p.privateMethod(); // 编译错误不可访问 p.finalMethod(); // 输出: Parent final (静态绑定) p.normalMethod(); // 输出: Child normal (动态绑定看实际类型Child) } }实操心得谨慎使用静态方法由于静态方法不支持多态过度使用静态方法会破坏面向对象的设计让代码变得僵化难以测试和扩展。通常只有工具类方法如Math.sqrt()或真正的类级别操作才适合声明为static。在涉及对象行为的场景下优先考虑实例方法。4. 多态性的高级应用与设计模式理解了基础原理我们来看看多态性在更复杂场景和经典设计模式中的应用。这才是它真正大放异彩的地方。4.1 基于接口的多态更松散的耦合相比于继承接口是实现多态更灵活、更推荐的方式。Java是单继承但可以实现多个接口。接口只定义行为契约不提供实现强制实现类完成所有方法从而实现更彻底的抽象。// 定义接口 interface Payment { boolean pay(double amount); String getProvider(); } // 多种实现 class AlipayPayment implements Payment { Override public boolean pay(double amount) { System.out.println(使用支付宝支付: amount 元); // 调用支付宝SDK... return true; } Override public String getProvider() { return Alipay; } } class CreditCardPayment implements Payment { private String cardNumber; public CreditCardPayment(String cardNumber) { this.cardNumber cardNumber; } Override public boolean pay(double amount) { System.out.println(使用信用卡尾号 cardNumber.substring(cardNumber.length()-4) 支付: amount 元); // 调用银行网关... return true; } Override public String getProvider() { return CreditCard; } } // 订单处理类完全依赖抽象接口 class OrderProcessor { private Payment payment; // 持有接口引用 public OrderProcessor(Payment payment) { this.payment payment; // 依赖注入 } public void processOrder(double amount) { System.out.println(开始处理订单支付方式: payment.getProvider()); if (payment.pay(amount)) { System.out.println(支付成功); } else { System.out.println(支付失败。); } } } // 使用 public class Shop { public static void main(String[] args) { // 可以轻松切换支付方式无需修改OrderProcessor OrderProcessor processor1 new OrderProcessor(new AlipayPayment()); processor1.processOrder(100.0); OrderProcessor processor2 new OrderProcessor(new CreditCardPayment(1234567812345678)); processor2.processOrder(200.0); // 未来新增微信支付只需创建 WeChatPayment implements Payment 即可 } }接口多态的优势解耦OrderProcessor只和Payment接口对话完全不知道AlipayPayment或CreditCardPayment的存在。可扩展性新增支付方式对现有系统零侵入。可测试性可以轻松创建MockPayment用于单元测试OrderProcessor。组合优于继承避免了继承带来的脆弱基类问题。4.2 策略模式多态的经典体现策略模式定义了一系列算法并将每个算法封装起来使它们可以相互替换。它让算法的变化独立于使用算法的客户。这本质上就是接口多态的完美应用。上面的支付例子就是一个简单的策略模式。Payment是策略接口AlipayPayment和CreditCardPayment是具体策略OrderProcessor是上下文Context它持有一个策略引用并负责调用策略。更复杂的例子排序策略// 策略接口 interface SortStrategy { void sort(int[] array); } // 具体策略A冒泡排序 class BubbleSort implements SortStrategy { Override public void sort(int[] array) { System.out.println(使用冒泡排序); // 实现冒泡排序算法... } } // 具体策略B快速排序 class QuickSort implements SortStrategy { Override public void sort(int[] array) { System.out.println(使用快速排序); // 实现快速排序算法... } } // 上下文数据处理器 class DataProcessor { private SortStrategy sorter; private int[] data; public DataProcessor(int[] data) { this.data data; } public void setSortStrategy(SortStrategy strategy) { this.sorter strategy; } public void process() { if (sorter null) { throw new IllegalStateException(未设置排序策略); } sorter.sort(data); // ... 其他处理逻辑 System.out.println(数据处理完成。); } } // 客户端可以根据数据特点动态选择策略 public class Client { public static void main(String[] args) { int[] smallData {5, 3, 1, 4, 2}; int[] largeData new int[10000]; // 假设填充了大量数据 DataProcessor processor1 new DataProcessor(smallData); processor1.setSortStrategy(new BubbleSort()); // 小数据用冒泡 processor1.process(); DataProcessor processor2 new DataProcessor(largeData); processor2.setSortStrategy(new QuickSort()); // 大数据用快排 processor2.process(); } }4.3 工厂模式与多态创建对象的艺术工厂模式利用多态来封装对象的创建过程使客户端代码与具体产品类解耦。// 产品接口 interface Logger { void log(String message); } // 具体产品 class FileLogger implements Logger { Override public void log(String message) { System.out.println(Log to file: message); } } class ConsoleLogger implements Logger { Override public void log(String message) { System.out.println(Log to console: message); } } // 简单工厂 class LoggerFactory { public static Logger getLogger(String type) { if (file.equalsIgnoreCase(type)) { return new FileLogger(); } else if (console.equalsIgnoreCase(type)) { return new ConsoleLogger(); } throw new IllegalArgumentException(Unsupported logger type: type); } } // 客户端代码 public class App { public static void main(String[] args) { // 客户端只需要知道Logger接口和工厂无需关心具体Logger的实现 Logger logger LoggerFactory.getLogger(file); logger.log(Application started.); logger LoggerFactory.getLogger(console); logger.log(This is a debug message.); } }虽然简单工厂里还有if-else但它的范围被限制在了工厂内部。客户端代码 (App类) 完全依赖于抽象的Logger接口符合“面向接口编程”的原则。更高级的工厂模式如工厂方法、抽象工厂会进一步利用多态来消除这些条件判断。避坑指南多态与instanceof的滥用多态的一大目标就是消除instanceof这样的类型检查。如果你在使用了多态的代码中仍然频繁使用instanceof来判断具体类型并进行强制转换那很可能意味着你的设计出了问题。例如// 糟糕的设计多态白用了 void process(Animal a) { if (a instanceof Dog) { ((Dog)a).fetch(); // Dog特有的方法 } else if (a instanceof Cat) { ((Cat)a).climb(); // Cat特有的方法 } a.makeSound(); }改进方法考虑是否可以将fetch()和climb()这些特有行为抽象到Animal接口中或许叫performAction()或者使用访问者模式等设计模式来处理对象结构中的不同类型元素。如果行为确实是子类特有的且父类无法合理抽象那么可能需要重新审视类之间的关系。5. 多态性在实际开发中的常见问题与排查即使理解了原理在实际编码中围绕多态依然会遇到一些棘手的问题。下面是一些典型场景和解决方案。5.1 问题试图调用子类特有方法这是新手最常掉进的坑。通过父类引用只能调用父类中定义的方法或重写的方法。如果你想调用子类独有的方法编译器会报错。class Parent { public void common() { System.out.println(Parent common); } } class Child extends Parent { Override public void common() { System.out.println(Child common); } public void childOnly() { System.out.println(Child only method); } } public class Main { public static void main(String[] args) { Parent p new Child(); p.common(); // 没问题输出 Child common // p.childOnly(); // 编译错误Parent类型没有childOnly方法 } }解决方案向下转型如果你确信该引用指向的对象实际是Child类型可以进行强制类型转换。if (p instanceof Child) { Child c (Child) p; // 向下转型 c.childOnly(); // 现在可以调用了 }注意不安全的向下转型会导致ClassCastException因此务必先用instanceof检查。重新设计思考这个“子类特有方法”是否真的应该只属于子类。如果多个子类都有类似的特有行为或许可以将其提升为父类的一个抽象方法或者定义在另一个接口中让需要的子类去实现。这更符合面向对象的设计原则。5.2 问题字段没有多态性在Java中字段成员变量的访问不具多态性它取决于引用变量的编译时类型。这是一个容易让人困惑的点。class Parent { String value Parent; } class Child extends Parent { String value Child; // 这实际上是隐藏了父类的value字段不是重写 } public class FieldTest { public static void main(String[] args) { Parent p new Child(); System.out.println(p.value); // 输出: Parent (看引用类型) System.out.println(((Child)p).value); // 输出: Child (看实际类型但需转型) } }原因与建议字段是静态绑定的。p.value在编译时就解析为Parent.value。实践中应尽量避免在子类中定义与父类同名的字段这会造成混淆和难以调试的bug。如果需要让子类提供不同的值通常的做法是将父类字段设为private通过受保护的方法如getter来访问。在子类中重写这个getter方法方法是动态绑定的返回子类特定的值。class Parent { private String value Parent; public String getValue() { return value; } // 提供访问方法 } class Child extends Parent { private String childValue Child; Override public String getValue() { return childValue; } // 重写方法实现多态 } // 使用 Parent p new Child(); System.out.println(p.getValue()); // 输出 Child5.3 问题构造器中调用可重写方法在构造方法中调用可被重写的方法是一种危险的做法可能导致程序行为异常。class Parent { public Parent() { System.out.println(Parent constructor); print(); // 危险在构造过程中调用可重写方法 } public void print() { System.out.println(Parent print); } } class Child extends Parent { private int value 10; public Child() { super(); // 隐含调用 System.out.println(Child constructor); } Override public void print() { System.out.println(Child print, value value); } } public class ConstructorDanger { public static void main(String[] args) { new Child(); } }输出可能是Parent constructor Child print, value 0 Child constructor发生了什么创建Child对象时首先调用Parent的构造器。在Parent构造器中调用了print()方法。由于多态性此时实际类型已经是Child所以调用的是Child.print()。但此时Child的构造器还未执行其字段value尚未被初始化int默认值为0因此打印出0。之后才执行Child的构造器将value赋值为10。解决方案绝对避免在构造器中调用非final、非private的实例方法。如果父类构造器需要执行一些初始化逻辑应该将这些逻辑放在private或final方法中或者提供一个独立的init()方法在对象完全构造后由客户端显式调用。5.4 多态与泛型的结合泛型提供了编译时的类型安全而多态提供了运行时的行为多样性。结合使用时需要注意一些边界情况。import java.util.*; class Animal { void feed() {} } class Dog extends Animal { void bark() {} } public class GenericPolymorphism { public static void main(String[] args) { // 集合的多态 - 没问题 ListAnimal animals new ArrayListDog(); // 编译错误泛型是不变的 // ListAnimal 不是 ListDog 的父类 // 正确的用法使用通配符 List? extends Animal list1 new ArrayListDog(); // 上界通配符安全的读取 // list1.add(new Dog()); // 编译错误不能写入除了null Animal a list1.get(0); // 可以读取为Animal List? super Dog list2 new ArrayListAnimal(); // 下界通配符安全的写入 list2.add(new Dog()); // 可以写入Dog及其子类 // Dog d list2.get(0); // 编译错误读取出来只能是Object Object obj list2.get(0); } }PECS原则Producer-Extends, Consumer-Super是处理泛型集合多态的核心口诀生产者Producer如果你需要一个集合提供元素读取使用? extends T。例如一个只用来遍历的集合。消费者Consumer如果你需要一个集合接收元素写入使用? super T。例如一个只用来添加元素的集合。理解并正确使用泛型通配符能让你的API在保持类型安全的同时拥有最大的灵活性。多态性不是一句简单的口号而是贯穿优秀软件设计始终的实践思维。从消除冗长的条件判断到构建基于接口的松耦合架构再到应用各种经典设计模式多态都在其中扮演着核心角色。它要求我们更多地思考“抽象”和“契约”而非具体的实现细节。刚开始可能会觉得有点绕但一旦你习惯了这种思维方式并尝到它带来的可维护性和扩展性的甜头就再也回不去了。下次当你手指不由自主地敲下if (obj instanceof ...)时不妨停下来想一想这里是否可以用多态来优雅地解决
返回列表