Java多态详解:从动态绑定到面试实战,彻底搞懂面向对象核心机制 1. 为什么Java面试总绕不开多态做Java开发这些年无论是面试初级岗位还是带团队做技术评审我几乎每次都会碰到“多态”这个话题。很多人能把“继承、重写、父类引用指向子类对象”这三句话背得滚瓜烂熟但真到写代码的时候却很少主动利用多态去优化自己的程序而到了面试官追问“动态绑定是怎么实现的”“多态解决了什么问题”的时候往往又开始支支吾吾。这么说吧多态是Java面向对象三大特性封装、继承、多态里最抽象、也是最难讲清楚的一个。封装讲的是“怎么把数据藏起来”继承讲的是“怎么把已有代码拿过来扩展”而多态讲的是“运行时怎么决定到底执行谁的方法”。前两个还算有实体概念可以抓多态则是纯粹的一种行为机制——它让同一个方法调用在不同对象上表现出不同的行为。理解多态往小了说是帮你通过面试、写对代码往大了说它是理解设计模式、框架源码、甚至阅读Spring这类大型项目的底层钥匙。如果你去看Spring的源码会发现里面到处都是接口引用指向不同实现类的写法如果不懂多态读起来会非常痛苦。这篇文章我就想用尽可能贴近实战的角度把Java多态拆开揉碎讲清楚它的原理、用法、常见坑以及怎么在真实项目中用好它。我不打算给你念教科书而是从我这些年写代码、看源码、刷面试题的实际经验出发把多态这个东西讲透。文章会覆盖从最基础的必要条件到进阶的动态绑定、虚方法表再到设计模式层面的应用以及面试高频题的应对思路。不管你是刚学Java的初学者还是准备跳槽的求职者或者写了几年代码但一直对多态有种“似懂非懂”感觉的朋友这篇文章应该都能给你一些不一样的东西。2. 多态的前置基础封装、继承与重写2.1 三者的分工与关系要理解多态绕不开“封装、继承、重写”这三个概念。很多人把“封装继承多态”当成一句口诀背却从来没细想过这三者其实是一条逻辑链条封装让你把数据和行为绑定到一个类里继承让你在已有类的基础上扩展出新的类而多态则是在继承的前提下让同一段逻辑能够处理不同类型的具体对象。你可以把这三者理解成造车的过程封装相当于设计了一台发动机的完整内部结构对外只暴露启动和停止的接口继承相当于在这台发动机的基础上衍生出了涡轮增压版、混动版、柴油版多态则相当于驾驶员只需要知道“踩油门车会加速”而至于具体是哪个版本的发动机在工作驾驶员根本不需要关心车自己会正确处理。这里我想强调一个很多初学者容易忽略的点封装和继承本身并不可怕它们都是静态代码结构上的变化而多态是动态行为的体现。也就是说多态只有在程序运行的那一刻才真正发生作用。你用父类类型的引用去调用一个方法编译期编译器根本不确定会执行哪个版本的方法只有在运行时JVM根据实际对象类型来决定调用哪个重写方法这就是多态的精髓。2.2 多态的两种形式Java里的多态严格来说分两种编译时多态和运行时多态。编译时多态其实就是方法重载——同一个类里多个方法名字相同但参数列表不同调用时编译器根据参数的类型和数量在编译期就能确定调用的具体是哪个方法。运行时多态则依赖方法重写和继承——调用哪个方法要等到运行时由JVM根据对象的真实类型来决定。说实话日常开发中我们讨论“多态”绝大多数时候指的是运行时多态。为什么因为编译时多态重载虽然很常用但它并没有体现出“面向对象的抽象能力”它更像是一种语法糖。而运行时多态才是面向对象设计的灵魂它让你能够面向抽象编程把不稳定、易变化的部分用接口隔离起来。重载和重写的混淆是面试里非常经典的考点我后面会单独用一节来讲清楚它们的区别这里先留个伏笔。3. 运行时多态的三个必要条件3.1 继承、重写与父类引用指向子类对象Java运行时多态必须同时满足三个条件要有继承关系或者实现了同一个接口子类必须重写父类的方法接口的话就是实现方法父类类型的引用指向子类的对象。这三个条件听起来简单但我在带新人的时候发现很多人只记住了条件不知道背后为什么非要这样。我逐一解释一下。第一个条件“继承或实现”是建立“父子关系”的基础。只有存在这种关系我们才能把一个子类对象赋值给父类类型的引用这种操作在Java里叫向上转型。第二个条件“重写”是多态的行为来源。如果子类不重写父类方法那调用该方法时所有对象的行为都一样多态也就不存在了。第三个条件“父类引用指向子类对象”这是多态发生的场景。注意之所以要强调“父类类型的引用”是因为Java编译器在编泽代码时是根据引用类型来检查方法是否存在、签名是否匹配的而运行时JVM则是根据对象真实类型来分派方法调用。用一个最简单的代码示例来演示// 父类 public class Animal { public void sound() { System.out.println(动物叫); } } // 子类1 public class Dog extends Animal { Override public void sound() { System.out.println(汪汪汪); } } // 子类2 public class Cat extends Animal { Override public void sound() { System.out.println(喵喵喵); } } // 测试 public class Test { public static void main(String[] args) { Animal a1 new Dog(); Animal a2 new Cat(); a1.sound(); // 输出汪汪汪 a2.sound(); // 输出喵喵喵 } }这段代码虽然基础但它是理解多态的钥匙。你留意到一个关键点没有a1和a2的编译期类型都是Animal但运行时它们的真实类型分别是Dog和Cat。调用sound()方法时JVM没有因为引用类型是Animal就调用Animal.sound()而是根据对象在堆里真实的类型标记分别调用了Dog.sound()和Cat.sound()。3.2 向上转型为什么父类引用能指向子类对象向上转型是Java继承体系里非常自然的类型转换方式。因为子类是一种特殊的父类Dog is an Animal所以把Dog对象赋值给Animal引用是完全安全的。这种转型是自动的不需要显式强制转换。向上转型的价值在于“统一处理”。你可以写一个接受Animal参数的方法不管是Dog、Cat还是未来新增的Sheep都可以直接传进去。这大大提升了代码的扩展性。但副作用也很明显通过父类引用你只能访问父类中定义的成员子类新增的方法和字段在编译期是不可见的。这就是为什么有些初学者写了a1.fetchBall()会报编译错误——fetchBall()是Dog类自己定义的方法Animal引用看不到它。面试里经常有这种坑题父类引用指向子类对象然后尝试调用子类独有的方法。很多人想当然认为“对象真实类型是Dog应该能调用”但实际编译期就报错了。记住编译看引用类型运行看对象类型这句话我建议你写在本子上。3.3 向下转型什么时候要转怎么安全地转既然向上转型限制了对子类特有方法的访问那如果需要调用子类特有的方法怎么办答案是向下转型。向下转型就是把父类引用强制转换为子类类型注意只有对象真实类型确实是那个子类时向下转型才能成功。Animal a new Dog(); Dog dog (Dog) a; // 安全因为a的真实类型就是Dog dog.fetchBall(); Animal b new Cat(); Dog dog2 (Dog) b; // 编译可以通过但运行时会抛出ClassCastException这个例子是典型的运行时异常陷阱。前两行没问题后面三行则会在运行时抛出类型转换异常。因为编译器只知道b是Animal类型它无法判断b的实际类型是不是Dog只能代码层面放行JVM在运行时检测到b的真实类型是Cat和Dog没有继承关系直接拒绝转换。在实际开发中向下转型往往跟instanceof搭配使用确保安全if (a instanceof Dog) { Dog dog (Dog) a; dog.fetchBall(); }JDK 16之前instanceof判断和强制转换是两次独立操作写完代码总觉得啰嗦。JDK 16正式引入了instanceof的模式匹配可以合并成一行if (a instanceof Dog dog) { dog.fetchBall(); }这里再提醒一个容易踩的坑instanceof的左边如果是null直接返回false不会抛异常。所以不用先判空再instanceof直接写就行。4. 重载与重写名字相似本质不同4.1 重载Overload到底怎么回事重载发生在同一个类中多个方法同名但参数列表不同。注意重载对返回值没要求也就是说你不能只靠返回值不同来区分重载方法因为调用时根本不知道你想拿哪个返回值。参数列表不同指的是参数类型、参数个数或者参数顺序不同。public class Calculator { 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; } }调用add方法时编译器根据你传入的参数类型和数量来决定调用哪个版本这属于编译期多态。这里有个常见的面试延伸题如果调用add(1, 2)但参数里一个int一个double比如add(1, 2.0)编译器会怎么处理答案是把1自动提升为double匹配add(double, double)版本。自动类型提升的规则在重载场景里常常带来意料之外的结果尤其当你同时有add(int, double)和add(double, int)的时候调用add(1, 2.0)反而会编译失败——编译器找不到唯一匹配合适的重载版本出现歧义。4.2 重写Override是怎么生效的重写发生在有继承关系的类之间子类对父类的方法进行重新实现。重写的规则比重载严格得多方法名、参数列表必须完全一致返回值类型可以相同也可以是父类返回类型的子类型协变返回访问修饰符不能比父类更严格比如父类是protected子类不能是private子类不能抛出比父类更宽泛的受检异常被重写的方法不能用final修饰被重写的方法不能用static修饰。JVM调用重写方法时走的是动态绑定路径后面专门讲。4.3 重载和重写的判定速查表对比维度重载Overload重写Override发生位置同一个类父子类之间方法名必须相同必须相同参数列表必须不同必须相同返回值不限相同或协变修饰符不限不能更严格绑定时机编译期运行期异常声明不限不能更宽泛这张表建议存到笔记里。面试题里经常给出代码片段然后问“输出是什么”本质就是在考你对重载和重写绑定时机的理解。我见过一个经典题目父类引用指向子类对象调用一个父类里定义为静态的同名方法输出是什么答案是静态方法不存在重写它属于类的调用时看引用类型所以会执行父类静态方法。这个坑每年都有人踩。5. 动态绑定多态背后的运行机制5.1 从符号引用到直接引用Java是一门编译和运行分层的语言。源代码编译成字节码后类文件里保存的方法调用指令最初以符号引用的形式存在。到了运行时JVM需要把符号引用解析为直接引用这个过程叫方法解析。难点在于对普通非static、非private、非final的实例方法JVM没法在类加载阶段就确定要调用的版本因为对象的类型要到运行时才能确定。所以JVM把这一步推迟到第一次方法调用发生时进行动态绑定——根据接收者的实际类型选择最合适的重写方法版本。5.2 虚方法表是干什么的动态绑定听起来玄乎但它的实现其实依赖一个叫虚方法表Virtual Method Table的结构。每个类在方法区里维护一张表表里按索引存放着该类的所有虚方法入口地址实际是方法入口在JVM内部的解析地址。子类继承父类时虚方法表会复制父类的方法入口然后对重写的方法替换成自己的入口。这样设计的好处是性能好。JVM做动态分派时通过对象头的类型指针找到类元数据再定位到虚方法表根据方法索引取出对应的方法入口直接调用。整个过程不涉及遍历查找时间复杂度是O(1)的。不过我要说明一下HotSpot VM实际采用的是接口方法表加虚方法表的组合方案纯虚方法表的说法是教学简化。你理解成“每个类有一张方法映射表子类重写后覆盖对应条目”就可以了没必要把实现细节钻得太深。但理解了这个机制你就能明白为什么final方法不能重写而且可以走静态绑定——final方法没有参与动态分派的必要编译器可以直接确定调用的版本这也是final方法能被内联优化的底层原因。5.3 方法分派静态类型和动态类型JVM的方法分派分静态分派和动态分派。静态分派发生在编译期主要用于方法重载根据静态类型决定调用哪个版本。动态分派发生在运行期主要用于方法重写根据动态类型决定调用哪个版本。这里有一个我特别想强调的细节当一个调用既有重载又有重写时编译阶段先用静态分派确定要调用的重载版本运行阶段再用动态分派在该版本范围内选择具体的重写实现。这个规则是理解一堆复杂面试题的基础。举个例子public class Parent { public void test(String str) { System.out.println(Parent String); } } public class Child extends Parent { Override public void test(String str) { System.out.println(Child String); } public void test(Object obj) { System.out.println(Child Object); } } // 调用 Parent p new Child(); p.test(hello);输出是什么答案是“Child String”。分析一下编译期p的静态类型是Parent编译器会在Parent类里找匹配test(String)的方法找到了标记为动态分派。运行期JVM根据p的实际类型Child查找test(String)的重写版本找到了Child.test(String)执行。注意Child.test(Object)和Child.test(String)是重载关系但因为编译阶段静态类型是ParentParent里没有test(Object)方法所以这个重载版本从一开始就没有参与候选。这个东西很有迷惑性我当年笔试就栽过一回。你完全可以自己写个小demo验证一下理解之后对重载重写和多态的理解会上一个档次。6. 多态的实战价值6.1 面向抽象编程消除分支判断不谈实战的多态科普都是空中楼阁。多态最重要的价值就是让你在代码里把“不同”的部分抽象成公共接口把变化隔离在一个个实现类里。举一个我实际经历过的情况早先在一家电商公司订单结算时需要按照不同类型计算运费什么普通订单、跨境订单、保价订单每种计算逻辑都不一样。第一次实现时写了一个OrderService里面全是if-elseif (order.getType() OrderType.NORMAL) { // 普通运费逻辑 } else if (order.getType() OrderType.CROSS_BORDER) { // 跨境运费逻辑 } else if (order.getType() OrderType.INSURED) { // 保价运费逻辑 }这些分支代码全挤在一个方法里到了迭代后期改一个订单类型的规则要在这个方法里找到对应的分支位置改动波及范围很大。后面接手人重构改用策略模式——本质就是多态public interface ShippingCostCalculator { double calculate(Order order); } public class NormalShippingCalculator implements ShippingCostCalculator { Override public double calculate(Order order) { // 普通运费逻辑 } } public class CrossBorderShippingCalculator implements ShippingCostCalculator { Override public double calculate(Order order) { // 跨境运费逻辑 } }然后通过一个MapOrderType, ShippingCostCalculator来按类型取出对应的策略执行新增订单类型时只需要新写一个实现类并注册完全不需要改动原有逻辑。这就是多态对代码结构的影响不是“用if-else判断类型执行分支”而是“把执行分发交给JVM的动态绑定”。我们把思想升华为面向抽象编程让高层模块不依赖于低层实现细节。6.2 模板方法模式的骨架模板方法模式也是多态的经典应用场景。父类定义算法的骨架把可变步骤设计成抽象方法由子类去实现。整个过程是父类的方法被调用内部执行过程中会触发子类重写的步骤方法。比如做一份报表导出的工具类步骤是查询数据、格式化、写到文件。三步骤顺序固定但每一步的具体实现根据导出格式不同而变化。父类把queryData和formatData设计为抽象方法writeFile作为通用步骤放在父类实现。子类继承后只需实现抽象方法就得到了完整的导出流程。这种设计思路的底层就是多态JVM在调用父类模板方法时方法内部去调用被重写的抽象方法动态绑定了子类的实现。6.3 测试替身让单元测试更容易多态在单元测试里同样发挥着作用。测试一个类时经常需要替掉它依赖的外部组件比如数据库访问、远程调用。把依赖设计成接口测试中就可以传一个Mock实现进去让被测目标专注于自身逻辑验证。这也是多态的价值——面向接口编程的好处之一就是系统变轻容易测试。很多团队强制要求代码分层Service层依赖Repository接口而不是具体实现类除了解耦还有个目的就是方便测试。如果你把所有依赖都写成具体类那测试时只能构造厚重的环境跑一次测试疲惫得很。7. 接口层面多态与抽象类选型对比7.1 接口是更纯粹的多态继承之外接口是实现多态的另一个重要途径。接口定义了一组行为规范实现类提供具体行为。Java 8之后接口支持default方法、static方法Java 9之后还支持私有方法接口的表述能力大大增强。接口层面多态的好处在于实现类可以同时实现多个接口不像类继承只能单继承这给设计提供了很大的灵活性。你在Spring里天天用的ApplicationContext、BeanFactory本质上都是接口容器运行时注入各种具体实现这就是接口多态在框架层面的大规模应用。7.2 抽象类与接口的选择困局什么场景用抽象类什么场景用接口面试必考不说日常设计也经常遇到。我的经验是分三点看如果多个类之间有显著的公共状态字段或公共方法实现考虑抽象类可以在父类里提供默认实现如果只是定义行为规范、不关心实现细节接口更合适如果既要共享代码又想保持多重能力可以组合使用抽象类实现接口然后具体类继承抽象类。比如动物的例子Flyable这个接口适合定义“会飞”的行为而Bird抽象类可以提供翅膀、产卵等公共特性Eagle继承Bird并实现Flyable结构上就非常清晰。7.3 一个容易被忽略的细节接口的字段默认是public static final也就是常量抽象类可以有实例字段。很多人忽视这个区别把不该出现在接口里的状态塞进去结果接口慢慢从行为规范变成了数据类。接口里放的是“能干什么”不是“屋里有什么”。如果你发现自己在一个接口里定义了十个字段、三个常量那大概率设计出了问题该用抽象类。我记得有一次Code Review看到同事把订单状态枚举直接定义在接口里理由是“方便引用”。这属于典型的接口污染后续接口一旦发布被实现这些字段就难改了。规范讲接口只做行为约定状态字段应该放到枚举、配置类或抽象类里。8. 泛型、协变与多态的边界问题8.1 泛型子类型关系和数组子类型关系的区别泛型和多态也有关联。很多人在学习泛型时被一个事实惊到ListDog并不是ListAnimal的子类型也就是说ListAnimal类型的变量不能接收ListDog对象。但数组恰好相反——Dog[]是Animal[]的子类型可以赋值。这个差异正是多态在数组和泛型领域的分水岭。数组支持协变但代价是运行时检查插入不兼容类型会在运行时抛ArrayStoreException。泛型则为了类型安全在编译期就限制了这种赋值避免了运行时风险。我们做设计时应该克制隐式的不安全行为尽量把问题在编译期解决。Animal[] animals new Dog[10]; animals[0] new Cat(); // 编译通过运行时报ArrayStoreException ListAnimal animalList new ArrayListDog(); // 编译报错泛型不支持协变 List? extends Animal list new ArrayListDog(); // 使用通配符实现协变第二行会编译失败第三行用受限通配符才能表达“某种Animal的子类型”。泛型的协变、逆变、不变三规则深挖起来又是一大篇但这里至少明白一点多态的继承关系不一定能直接套到泛型类型上泛型类型参数间的父子关系不会传递到容器类型上。8.2 协变返回类型重写时也可以变宽泛协变返回类型是多态在方法重写时的扩展。JDK 5之后重写方法时返回值类型允许是原类型的子类型。public class Factory { public Product create() { return new Product(); } } public class SpecialFactory extends Factory { Override public SpecialProduct create() { return new SpecialProduct(); } // SpecialProduct是Product的子类 }这种设计让子类在重写时可以提供更具体的返回类型调用方如果持有SpecialFactory引用就能直接用SpecialProduct接收结果减少一次向下转型。我在阅读一些框架源码时经常看到这种写法它可以优化类型信息流值得在API设计时考虑。9. 面试高频考点与易踩坑整理9.1 多态相关的典型面试题我把这些年碰到的、以及和同行交流收集到的面试题做了个梳理集中回答高频的几个。第一题多态的必要条件。答案前面已经写过继承、重写、父类引用指向子类对象。第二题重写和重载的区别。用前面的速查表回答重点是绑定时机不同重载编译期决定重写运行期决定。第三题多态的实现原理。可以从方法解析、动态分派、虚方法表三个层面展开最后落到“JVM在运行时根据对象实际类型选择方法”这一句。第四题静态方法可以被重写吗答案是不能静态方法跟类绑定可以被隐藏但不会被重写。隐藏和多态无关调用时只跟引用类型有关。第五题构造方法能被重写吗不能。上一节已经解释过构造方法不是普通方法子类有自己的构造方法链。不过要注意子类构造过程中会隐式调用父类构造方法这是继承机制的一部分。第六题私有方法能被重写吗不能。private方法压根不会被子类看到谈不上重写。子类定义同名同参的方法也只是新方法。第七题向上转型和向下转型的区别、安全性。向上安全自动向下需要经过类型检查遵守“编译看左边运行看右边”的原则。9.2 面试作答框架参考面试被问多态的时候不要上来就背概念建议按这个层次组织答案先一句话定义再说明前提条件接着用一段代码说明行为然后解释底层原理最后落到设计价值。这种由浅入深的结构既能把基础分拿到又能在深挖时体现水平。比如对方问“说说你对Java多态的理解”你可以先说“多态是同一行为在不同对象上表现出不同形态的机制”然后介绍三个必要条件接着给出Animal/Dog/Cat的小例子说明现象再解释动态绑定和虚方法表最后落脚到面向抽象编程和开闭原则。这个答案如果能流畅讲完面试官对这块基本就放心了。9.3 高频易踩坑清单我在真实开发里踩过、也看到别人踩过的坑集中在下面几个场景滥用向下转型。有人拿到父类引用后疯狂用instanceof加强转逐渐把多态的优势抵消了。如果代码里频频出现“看类型做不同事”通常说明抽象设计有缺陷。应该回到接口层面思考是不是可以把行为下沉到子类。在构造器里调用可重写方法。这是个大坑子类对象构造过程中父类构造器先执行这时如果父类构造器内部调用了被子类重写的方法实际执行的是子类版本的方法但子类字段此时还没初始化极容易拿到null或默认值。典型的先有鸡还是先有蛋问题。解决方式构造器里只调private或final方法。把字段隐藏当成重写。子类可以定义和父类同名字段但这不叫重写叫隐藏。字段的访问是根据引用类型在编译期决定的不存在动态绑定。如果你在两个类里定义了同名字段代码后期维护分分钟出事。建议用getter/setter代替字段直访或者给子类字段起不同的名字。对null调用多态方法。父类引用可以指向null对象但你说((Animal) null).sound()是非法的因为调用实例方法需要对象存在。这是基本的还是经常被面试官当成排查题考程序员有没有出现空指针的敏感度。Animal a null; if (a instanceof Dog) { // falsenull安全判断 System.out.println(这是狗); }注意上面这个输出不会打印。instanceof对null返回false不要往相反的方向理解。再补充一个异常场景如果重写方法里调用了可抛受检异常但没捕获的问题已经被编译器拦了这里不做展开。总之重写方法尽量不要抛出比父类版本更宽泛的受检异常否则接口层面抽象会偏差。10. 排查思路多态相关Bug怎么定位10.1 首先确认编译期与运行期的分界遇到一个与多态有关的问题第一步先判断问题发生在编译期还是运行期。编译期报错一般是类型引用越界运行期报错主要是类型转换、空指针、动态分派结果不符合预期。举个例子如果代码报“找不到符号”或者“类型不兼容”多半是静态类型层面向上转型后访问子类特有成员检查那里是否缺乏向下转型或方法是否定义在了正确层。如果报ClassCastException则是向下转型目标类型和对象真实类型不一致需要对instanceof进行检查。10.2 用好断点和IDEA的能力定位多态问题IDE的调试工具其实是把利器。IDEA里在方法调用点打上断点右键呼出Evaluate Expression可以查看调用目标方法的接收者究竟是什么类型——在Debugger窗口的Variables面板里能看到变量的静态类型显示为声明类型而实际类型通常显示为具体子类名。你可以直接展开它的class字段确认对象真实类型。IDEA还有一项功能叫“Show Execution Point”顺着调用栈往上走可以反推出是在哪一个调用链上进入到了当前方法的。对于复杂的多态调用我一般会在入口处加日志打印对象类型的getClass().getName()快速确认运行时类型流转。10.3 整理成排查速查表现象可能原因排查方向调用了父类方法而非子类覆盖后的版本没有重写、子类方法签名不一致、忘加Override检查子类方法签名和父类是否完全匹配编译报“找不到符号”向上转型后试图调用子类独有方法考虑是否该向下转型或提升方法到父类运行时报ClassCastException向下转型类型不匹配用instanceof先判断再转换调用子类方法时字段为null构造器中调用了可重写方法子类字段未初始化构造器中只用private/final方法重载方法调用结果奇怪参数类型自动提升导致匹配到不期望的重载版本手工指定类型或统一参数设计无法继承某个类类被final修饰检查设计是否确实需要final或改为组合方式这张表是我平时给组内同事排查问题时用的你可以直接拿去参考。多态问题大多不是算法复杂而是类型层次复杂关键是厘清“编译期能确认什么、运行期才知道什么”这条主线。11. 一些面向长期竞争力的建议工程上用好多态不只是一个语言层面的话题它背后关联到设计原则、代码演进和可维护性。我个人在这几年的实践中积累了一些体会分享出来供参考第一多用接口引用少用具体类引用。局部变量、方法参数、返回类型只要不依赖具体实现细节都优先用接口类型声明。你在写代码时可能会觉得多打了个名字但这一个习惯能大幅度提高代码弹性。我见过太多早期设计图省事直接使用具体类后面加需求时被迫改动大量调用点的情况。接口是一个便宜的保险付出的只是每次的“多敲几个字符”。第二写测试类的时候天然就在用多态。你定义的测试替身无论是手写还是Mockito生成本质上都要求被测代码能接受接口而不是写死的具体类。所以如果你的类依赖了具体实现导致无法测试重构的方向往往不止一个第一个动作通常是解开具体依赖引入接口。第三不要为了“展示技术”硬造多态结构。多态和设计模式一样是服务于可维护性的工具不是炫技的舞台。一个三层嵌套的抽象层次可能看起来高级但如果没有真实的扩展需求支撑这种过度设计会让后来维护的人头秃。好的多态设计是顺着业务自然长出来的不是凭空设计出来的。第四阅读框架源码时注意捕捉多态的影子。Spring的Bean生命周期、MyBatis的插件机制都大量运用了接口多态。看懂了一处再看别的框架就会触类旁通。可以从一个小点切入比如Spring的BeanPostProcessor接口它就是一个极其简洁的多态应用但支撑起了整个Spring AOP和初始化扩展体系。如果你打算长期做Java开发可以在多态上花的这些时间一定会在后续读源码、做重构、应对面试时连本带利地还回来。