☰
Java多态详解:从原理、优势与坑到综合练习
2026/10/12 13:14:14 网站建设 项目流程

如果你已经跟着这个系列学到这儿,说明Java基础语法、面向对象、继承和重写这些关都过了。但我猜你现在的状态跟我当年差不多:继承好像懂了,重写也会写了,一提到“多态”总觉得它神神秘秘,好像所有框架都在用,又好像自己写的练习代码里根本用不上。这篇文章就专门把多态这件事讲透——它的优势是什么、劣势又是什么、什么时候该用什么时候别硬用,最后用一个综合练习把这一整块知识串起来。学完这篇,你再看别人代码里的抽象类、接口、父类引用,就不会再发怵了。

1. 多态到底是怎么回事:先抛开术语说人话

很多教材上来就给定义:多态是“同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果”。这句话没错,但对初学者来说等于没说。我用一个更直白的说法:多态就是“用父类的变量,调子类的方法”。

举个例子,假设你有一个父类Animal,里面定义了一个方法makeSound()。子类Dog把它重写成打印“汪汪”,子类Cat把它重写成打印“喵喵”。如果某段代码里声明了一个Animal类型的变量a,这个变量既可以指向Dog对象,也可以指向Cat对象。当你调用a.makeSound()的时候,程序运行时才会根据a实际指向的对象来决定打印狗叫还是猫叫。

其实你没写多态也一样能实现这个效果,无非是加一堆if判断:

if (a instanceof Dog) { ((Dog) a).makeSound(); } else if (a instanceof Cat) { ((Cat) a).makeSound(); }

但等你接触Spring、MyBatis这些框架,会发现它们底层到处都在用“父类引用 + 重写方法”这种组合。原因很简单:框架不知道你将来要写什么类,它只能面向一个抽象层编程。你负责实现具体的子类,框架负责在运行时调用。这就是多态的核心价值——把“调用方”和“被调用的具体类型”解耦。

1.1 学多态之前,你得先确保两件事

第一,继承关系要清晰。多态的前提是必须有父类/接口和子类,没有继承和实现关系,多态无从谈起。第二,要理解方法重写(Override)。子类继承父类方法后,需要按自己的需求重新实现。这里有一个看起来很基础但非常重要的细节:重写要求方法签名完全一致,包括方法名、参数列表、返回值类型。一旦签名不一致,你以为自己在重写,实际变成了重载,程序结果就完全不是你想的那样了。

另外多说一句,多态在Java里主要有两种形态:一种是“编译期多态”,也就是方法重载,同一个类里多个同名方法、不同参数列表,编译器在编译时就能确定要调哪个;另一种是“运行期多态”,也就是我们这篇文章讨论的重点——方法重写配合父类引用,编译时编译器只知道调的是哪个方法名,但真正执行哪个子类的实现,要等程序跑起来才知道。

很多初学者把重载和重写搞混,考试都能混过去,一写代码就露馅。记住一句话:重载是“同一个类里的事儿”,重写是“父子类之间的事儿”。多态讲的是重写,不是重载。

1.2 把“变化”交给运行时,而不是交给if-else

为什么说运行时决定调用哪个方法是件好事?因为软件最大的敌人不是写得慢,而是需求变来变去。你今天写了if (animal instanceof Dog),明天加一个Bird类,就得改原来的代码;后天加一个Fish类,又得改。每加一个新类型,你都要在原来的判断逻辑上动刀,改着改着就把别的逻辑改坏了。

多态的思路是:未来新增的类型都去实现同一个父类方法,原有的调用代码一个字符都不用动。程序跑起来遇到新的子类对象,自动就调用到了它自己的实现。你从“改代码”变成了“加代码”。这两件事在项目维护里的体验完全不是一个量级。

2. 多态的优势:为什么一个普通练习也能看出差距

光说概念没意思,我们写一个实际的场景来对比。假设你在开发一个员工薪资计算功能,公司里有三种员工:普通员工、经理、实习生。每个人的薪资计算规则不一样。第一种写法,不用多态:

public class SalaryService { public double calculate(Employee e, int type) { if (type == 1) { // 普通员工:底薪 return e.getBaseSalary(); } else if (type == 2) { // 经理:底薪 + 绩效奖金 return e.getBaseSalary() + e.getPerformanceBonus(); } else if (type == 3) { // 实习生:固定津贴 return 3000; } return 0; } }

这个代码的问题非常明显:type == 3这个分支里,e这个参数根本没用到,实习生就不该是这里的调用者;而如果公司新招一种“外包员工”,你就得改这个calculate方法再加一个else if。更难受的是,将来每个员工的类型都对应一套完整业务逻辑,这个方法会越写越长,最终变成一个几千行没人敢动的“上帝方法”。

用多态重构一下。抽象类:

public abstract class Employee { private String name; protected double baseSalary; public Employee(String name, double baseSalary) { this.name = name; this.baseSalary = baseSalary; } public abstract double calculateSalary(); public String getName() { return name; } }

三个子类各自重写:

public class RegularEmployee extends Employee { public RegularEmployee(String name, double baseSalary) { super(name, baseSalary); } @Override public double calculateSalary() { return baseSalary; } } public class Manager extends Employee { private double performanceBonus; public Manager(String name, double baseSalary, double performanceBonus) { super(name, baseSalary); this.performanceBonus = performanceBonus; } @Override public double calculateSalary() { return baseSalary + performanceBonus; } } public class Intern extends Employee { public Intern(String name) { super(name, 3000); } @Override public double calculateSalary() { return 3000; } }

这时原本那个薪资服务类变成:

public class SalaryService { public double calculate(Employee e) { return e.calculateSalary(); } }

你没看错,核心逻辑就这一行。以后再增加“外包员工”“兼职员工”,只需要新建一个类继承Employee并重写calculateSalary(),SalaryService想都不用想。**这是多态最核心的优势:对扩展开放,对修改关闭。**框架程序员们天天说“开闭原则”,落到代码层面其实就是这么一回事。

2.1 扩展新功能时,旧代码终于不用再动

继续用上面这个例子说。一家公司刚开始只有普通员工,后来才有经理、实习生。用传统if-else的写法,每加一种角色就要改薪资服务类的源码,至少要加一个分支。试用期都过去三轮了,你还在同一个类里到处加else if,代码越来越脆。

用多态之后,流程变成了这样:新员工类型 → 新建一个类 → 继承Employee→ 实现calculateSalary()。原来的类变得极其稳定,几乎不需要碰。这在多人协作的项目里特别重要——你不需要理解别人写的几千行逻辑,只需要参考现成的子类复制一个结构,填自己的代码就行。别人也不需要因为你加了一个新类而重新测试旧的薪资服务。

2.2 让不相关的类看起来“同一种样子”

多态还有一个很容易被忽略的好处:它能抹平类型差异,让调用方只关心“这个东西能不能做某件事”,而不关心“它具体是什么”。

比如你要开发一个系统,需要把一堆不同格式的报表导出成PDF。Excel报表、Word报表、图片报表,它们的导出逻辑完全不同。如果不用多态,你得写一个if (obj instanceof ExcelReport)去挨个判断。用多态以后,你只需要定一个接口ReportExporter,里面放一个export()方法,让三种报表类分别实现。调用方只要遍历这些对象,统一调用export()就好。你不需要知道它内部是用了Apache POI还是PDFBox,也不需要在调用方引入任何具体类型的判断。

这也是为什么Java里那么多框架都爱用“接口 + 实现类”这套组合拳——调用方只依赖接口,具体实现可以随时替换,甚至可以由外部配置决定。你写自己的小工具时可能觉得无所谓,但一旦代码规模上来,“依赖抽象而不是依赖具体”这一个习惯就能帮你省掉无数改代码的时间。

2.3 代码复用和一致性维护跟着一起解决了

多态让公共逻辑集中在了父类里。比如上面例子中,getName()方法、name字段都写在父类Employee里,所有子类自动继承。将来如果我们要给所有员工加一个getEmployeeId()方法,也只要在父类里加一次就行。

更妙的是,当你有大量子类对象需要统一处理时,多态保证它们的行为都是一致的。比如公司季度大会,要遍历所有员工展示个人信息:

for (Employee e : employeeList) { System.out.println(e.getName() + " 的薪资是:" + e.calculateSalary()); }

有没有发现,employeeList里存的是经理、普通员工、实习生的混合体,但你完全不用关心具体是谁。这种代码读起来像在读业务文档:遍历所有员工,显示姓名,计算薪资。干干净净。这才是多态真正“香”的地方——让你的代码从“怎么实现”变成了“要做什么”的层面。

3. 多态的劣势与暗坑:不是所有地方都适合用

网上讲多态的文章大多在吹它的好处,很少提它的代价。但实际写代码的时候,因为用错多态而吃亏的情况真不少。我不是劝你别用多态,而是要你学会识别它不适用的场合。

3.1 性能开销:动态分派没那么便宜

先从争议最少的说起。Java方法调用默认是虚方法调用,子类重写了父类方法后,程序运行时需要根据对象实际类型去查找应该调用哪个方法实现,这个查找过程叫“动态分派”。它是JVM在运行期完成的,比编译期直接确定方法地址要慢一些。

当然,现代JVM有内联缓存(Inline Cache)之类的优化,大多数情况下这个性能差异小到可以忽略。但如果你写的是极端高性能场景——比如每秒循环上千万次的算法、实时图形渲染的每一帧更新,那么在热循环里频繁触发多态调用,性能损耗就会被放大。

这个坑怎么避呢?第一,优先保证代码清晰,在明确检测到性能瓶颈之后再做优化。第二,如果你自己设计一个底层工具类,内部频繁调用的方法尽量少用多态,可以用final关键字禁止父类方法重写,或者用private方法,这样编译器/JIT可以更激进地优化。记住,多态是写给人看的,不是写给机器看的。机器需要的只是速度,人需要的是结构。

3.2 可读性:代码跳来跳去,新手真的很崩溃

多态有一个被很多人忽略的“隐形成本”:阅读成本高。当你看到e.calculateSalary()这样一行代码时,你没办法立刻知道它到底执行了哪个类的实现。你得先看e的类型,再找到Employee抽象类,再根据业务逻辑推测当前运行时e是Manager还是Intern,然后去对应的子类里翻代码。如果要查的类有四五层继承关系,这趟“翻山越岭”就非常折磨人。

有人说“IDE能跳转啊”,可跳转出来的是抽象类里的声明,你还得手动找具体实现。类少还好,类一多就麻烦。所以在实际项目里,我见过不少团队因为滥用多态,把本来简单的一段业务逻辑拆得七零八落,每个类都很“干净”,但连起来简直像拼图——少看一个子类都不知道整个流程走的是什么。

我的建议是:**如果一个多态结构不能让你在5分钟之内讲清楚协作关系,那它就太复杂了。**能用简单的if-else解决的问题,没必要为了“优雅”硬造一个抽象层。抽象层是有成本的,它替你管理“变化”的同时,也增加了一层理解负担。

3.3 强制类型转换:多态的伴生事故

用多态的时候,父类引用往往只能调用父类中声明过的方法。如果你确实需要用子类特有的方法,你得做强制类型转换,也就是我们常说的向下转型:

Employee e = new Manager("张三", 8000, 5000); double bonus = ((Manager) e).getPerformanceBonus();

这种代码一旦多了,类之间就会隐含着一种“我知道你实际是谁”的强依赖。更危险的是,如果转型错了,比如把一个Intern强转成Manager,程序运行时会直接抛出ClassCastException,程序当场崩溃。

所以在写转型代码之前,一般要先用instanceof检查实际类型。可一旦你发现代码里到处是instanceof和强制转型,就要警惕了:这通常说明你的抽象层设计得不够好,或者你压根就不该在这里使用多态。多态的目的就是避免调用方关心具体类型,如果调用方处处都要关心,那不是你的用法有问题,就是抽象层选错了。

3.4 构造方法里调重写方法:最隐蔽的坑

这个坑我亲眼见过好几个初学者踩进去爬不出来。假设父类的构造函数里有这样一段代码:

public abstract class Employee { protected String badgeInfo; public Employee() { this.badgeInfo = buildBadgeInfo(); // 调用了一个可被子类重写的方法 } protected String buildBadgeInfo() { return "base-badge"; } } public class Manager extends Employee { private String level; public Manager(String level) { super(); // 先走父类构造 this.level = level; } @Override protected String buildBadgeInfo() { return "manager-badge-" + level; // level 还没初始化! } }

执行这段代码时会发现,badgeInfo的值变成manager-badge-null,而不是你预期的manager-badge-Gold。原因在于:new Manager("Gold")的时候,先执行父类构造函数,而父类构造函数里的buildBadgeInfo()被子类重写了,Java的动态分派机制会去调用子类的那个版本。但此刻子类自己的level字段还没赋值,所以拿到的是null。

这就是多态“运行时才决定方法版本”带来的副作用。规避方法很简单:构造函数里永远不要调用被子类重写的方法。如果父类构造时确实需要一些可变化的数据,建议改用参数传递,或者把逻辑挪到子类构造函数中去完成。这类问题编译期不会报错,程序也不会立刻崩溃,但结果就是不对,排查起来特别耗时间。

3.5 不是所有东西都能多态:成员变量和静态方法

这个小陷阱我在做题时都栽过一次。Java里只有实例方法具备多态性,成员变量和静态方法是不参与多态的。看下面这段:

public class Employee { protected String displayName = "员工"; } public class Manager extends Employee { protected String displayName = "经理"; } public static void main(String[] args) { Employee e = new Manager(); System.out.println(e.displayName); // 输出的是“员工” }

听上去很反直觉对吧?e明明指向一个Manager对象,你访问displayName这个成员变量时,得到的却是父类的值。因为成员变量是按“变量的声明类型”来访问的,编译期间就绑定了,跟运行时对象是谁没关系。静态方法同理,“父类引用调用静态方法”时走的也是声明类型的方法实现。所以你要是想依赖多态去切换静态行为,逻辑根本不成立。这个知识点虽然看起来像考试题,但在调试老代码时非常实用——看到输出跟预期不一样,先检查是不是在访问成员变量。

4. 综合练习:写一个员工薪资展示系统

前面讲了这么多,现在动手做一道稍微完整的练习。把你学过的封装、继承、重写、多态全部串起来。建议你自己先写一遍再往下看,遇到卡住的地方就是你没掌握的知识点,正好补一补。

4.1 练习需求

某公司要开发一个员工信息管理系统的最小模块,需求如下:

  1. 有抽象类Employee,包含员工姓名name、基础工资baseSalary,提供getName()方法。
  2. 有一个抽象方法calculateSalary(),用于计算员工实际工资;一个普通方法getInfo(),返回员工姓名和实发工资的格式化字符串。
  3. 三个具体员工类:
    • RegularEmployee:普通员工,工资只有基础工资。
    • Manager:经理,工资 = 基础工资 + 绩效奖金performanceBonus。
    • Intern:实习生,工资为固定的实习津贴internAllowance(不使用基础工资)。
  4. 写一个Company类,内部维护一个员工列表,提供addEmployee(Employee e)方法。
  5. 写一个主程序Main,创建一家公司,加入若干员工,遍历所有员工打印每位员工信息,并统计公司所有员工的总工资。

注意:不许在Company内部使用任何instanceof或强制类型转换,所有计算必须通过多态完成。

这个练习比表面上看起来有讲究。先分析一下需求的考察点:第一,calculateSalary()是一个抽象方法,每个子类都必须实现,这是重写的基本功;第二,Company的addEmployee参数类型是抽象类Employee,这就要求你理解“父类引用指向子类对象”;第三,遍历并调用calculateSalary()时,必须有运行期多态才能自动调用到正确的工资计算逻辑;第四,Intern的工资直接写死津贴值,说明子类可以和父类定义的字段行为不一样,考验你对抽象方法的理解。

4.2 设计思路

拿到需求先别急着敲代码。我的习惯是先用文字把类的结构列清楚:

Employee(抽象类) - name: String - baseSalary: double - getName(): String + calculateSalary(): double(抽象方法) + getInfo(): String → 返回 "姓名:xxx,本月工资:xxx元" RegularEmployee extends Employee + 构造方法:(name, baseSalary) + calculateSalary(): 返回 baseSalary Manager extends Employee - performanceBonus: double + 构造方法:(name, baseSalary, performanceBonus) + calculateSalary(): 返回 baseSalary + performanceBonus Intern extends Employee + 构造方法:(name) + calculateSalary(): 返回 3000(固定津贴,不理会 baseSalary) Company - employees: List<Employee> + addEmployee(Employee e) + printAllEmployees(): void + calculateTotalSalary(): double Main - main(...)

这个结构非常直观地展现了多态的使用场景:Company类只关心Employee这个抽象类型,不关心具体子类是谁。将来公司增加“外包员工”“退休返聘员工”,只需要新增一个类,Company零改动。

4.3 从抽象类到实现类,一步步写下代码

先写Employee抽象类。注意构造方法的可见性,我选了protected让子类可以访问baseSalary字段,同时对外提供getName():

public abstract class Employee { private String name; protected double baseSalary; public Employee(String name, double baseSalary) { this.name = name; this.baseSalary = baseSalary; } public String getName() { return name; } public abstract double calculateSalary(); public String getInfo() { return "姓名:" + getName() + ",本月工资:" + calculateSalary() + "元"; } }

接下来是三个子类:

public class RegularEmployee extends Employee { public RegularEmployee(String name, double baseSalary) { super(name, baseSalary); } @Override public double calculateSalary() { return baseSalary; } } public class Manager extends Employee { private double performanceBonus; public Manager(String name, double baseSalary, double performanceBonus) { super(name, baseSalary); this.performanceBonus = performanceBonus; } @Override public double calculateSalary() { return baseSalary + performanceBonus; } } public class Intern extends Employee { public Intern(String name) { super(name, 0); } @Override public double calculateSalary() { return 3000; } }

注意Intern的写法:调用父类构造方法时传入baseSalary = 0,但calculateSalary()直接返回3000。这没问题,因为父类的baseSalary只是一个字段,子类重写calculateSalary()后,字段有没有用得上不取决于定义,而取决于你的实现。这里可能会有人疑惑:那直接把baseSalary设成3000不就行了吗?也可以,但如果语义上是“实习津贴”,用一个固定常量更贴近真实业务。

然后是Company类:

import java.util.ArrayList; import java.util.List; public class Company { private List<Employee> employees = new ArrayList<>(); public void addEmployee(Employee e) { employees.add(e); } public void printAllEmployees() { for (Employee e : employees) { System.out.println(e.getInfo()); } } public double calculateTotalSalary() { double total = 0; for (Employee e : employees) { total += e.calculateSalary(); } return total; } }

最后是主程序:

public class Main { public static void main(String[] args) { Company company = new Company(); company.addEmployee(new RegularEmployee("张三", 6000)); company.addEmployee(new Manager("李四", 10000, 8000)); company.addEmployee(new Intern("王五")); company.addEmployee(new Intern("赵六")); company.printAllEmployees(); System.out.println("公司总工资:" + company.calculateTotalSalary() + "元"); } }

运行结果:

姓名:张三,本月工资:6000.0元 姓名:李四,本月工资:18000.0元 姓名:王五,本月工资:3000.0元 姓名:赵六,本月工资:3000.0元 公司总工资:30000.0元

这个练习的全部精华就在Company类的两段循环里:它不知道也不关心列表里到底坐着什么员工,只管挨个调用calculateSalary()。程序跑起来之后,JVM自己找对方法,该加奖金的加奖金,该发津贴的发津贴。你回头再想想最初那版if-else的SalaryService,是不是感觉完全不一样了?

4.4 练习完成后,再给自己加两道思考题

如果做完了觉得没难度,这里有两道思考题可以自查:

第一道:如果公司增加一个PartTimeEmployee(兼职员工),工资按小时计算,需要在小时间数和时薪的基础上算总额。你会怎么改?正确做法是新建一个类,继承Employee,重写calculateSalary(),然后把Company和Main里对应的创建逻辑加上去。如果你发现自己忍不住想去改Company类,说明你对多态的优势还没完全消化。

第二道:如果Employee抽象类的getInfo()里不是调用calculateSalary(),而是直接访问baseSalary字段,会出现什么问题?答案是:所有子类打印出来的工资都是baseSalary原始值,经理的绩效奖金、实习生的固定津贴全都体现不出来。这个问题的本质就是提醒你——多态只会发生在方法调用上,字段不管。想让行为跟着对象走,就必须走方法。

5. 练习常见错误与自查清单:这些坑比你想的更典型

每次给初学者讲解这道练习,我都会反复看到同几类错误。这些错误非常典型,基本覆盖了多态学习的大部分雷区,我把它整理成一张自查清单。

5.1 常见错误一:重写签名写错了,变成了重载

比如有人把calculateSalary()写成了:

public double calculateSalary(int month) { return baseSalary; }

参数列表从无变成了int month。编译器不报警,因为这不满足重写条件,Java会认为你在子类里的方法是一个全新的方法。结果Company循环里调用的calculateSalary()还是父类的抽象实现——如果父类实现了的话。但这里抽象方法没有实现,子类再用这个签名,编译器就会直接报ClassCastException的兄弟错误:子类没有被抽象方法完全实现,类无法编译通过。这种错误其实还算好,因为编译器给了明显提示。麻烦的是父类方法不是抽象方法的情况,比如:

public class Employee { public double calculateSalary() { return 0; } } public class Manager extends Employee { public double calculateSalary() { return baseSalary + performanceBonus; } }

这时候你写错了签名,比如加了一个参数,编译不会报错,但运行时Company永远调用的是父类的calculateSalary(),结果永远是0,排错能排一晚上。避免这个坑最有效的办法:在子类重写方法上加上@Override注解。如果你写错了签名没有构成重写,编译器会立刻警告,你就能在编译期把问题拦截下来。这是每个Java程序员都应该养成的小习惯。

5.2 常见错误二:在Company里忍不住用instanceof

有个学员写的Company类长这样:

public void printAllEmployees() { for (Employee e : employees) { if (e instanceof Manager) { System.out.println(((Manager) e).getManagerInfo()); } else if (e instanceof Intern) { System.out.println("实习生:" + e.getName()); } else { System.out.println(e.getInfo()); } } }

这种做法不是完全不能用,但它完全违反了这道练习的设计意图。你一旦开始判断具体类型,就说明getInfo()这个方法在抽象层里设计得不够用,你就跑去依赖具体子类。比如这里如果你需要“经理打印专属信息”,更好的方案是在抽象类里把getInfo()也设计成可重写的,让Manager自己去打印专属内容,Company还是无脑遍历调用getInfo()。这样代码会更干净,而且Company不需要理解业务细节。

5.3 常见错误三:把多态当成“到处建父类”的理由

反方向的问题也很多。有的同学练完多态之后觉得“哇太强了”,二话不说把所有类都塞到一个抽象父类下面,哪怕类与类之间八竿子打不着,也硬造继承关系。

比如搞一个抽象类BaseObject,让Car、User、Order都继承它,里面的抽象方法叫process()。这看起来很“多态”,实际上子类之间毫无共性,抽象方法的签名完全是为了凑而凑,真正调用的时候要么空实现要么写一堆错误逻辑。多态必须建立在“确实是同一类事物的不同形态”这个语义基础上,Dog和Cat都是Animal没问题,但如果你为了让代码“看起来面向对象”而硬造父子关系,项目后期刹车都刹不住。

5.4 自查清单

做这道练习的过程中,你不妨对照下面的清单过一遍:

  • 每个子类是否都完整实现了抽象方法?有没有漏掉@Override?
  • Company类里有没有出现instanceof或强制类型转换?出现了就说明设计有问题。
  • Intern的baseSalary传0是否合理?如果改成传3000,又会有什么不同?
  • 把List<Employee>的泛型改成List<Manager>,程序还能跑吗?为什么?
  • 如果在Employee里新增一个抽象方法,所有子类是否都被强制要求实现?这样的设计是好是坏?
  • 你能否在不改Company类的前提下,给公司加一种新的员工类型?

这些问题的答案如果你都能说清楚,多态这个知识点就算过关了。

5.5 从练习到实战:接口是不是比抽象类更适合多态?

练习里用的是抽象类,但实际上Java里还有一种更常用的方式——接口。很多同学会混淆“什么时候用抽象类,什么时候用接口”。我的个人经验是:如果多个类之间存在公共代码和字段,用抽象类;如果只是要约束“具备某种能力”,用接口。比如Animal抽象类可以持有name、age这些公共字段,而Swimmable接口只规定你“必须能游”,至于怎么游、游多快,接口不管。

从“从零开始学Java”的进度来说,你现在只需要知道两者都可以实现多态,等学到接口那一篇再去深究。如果你在看框架源码或者别人的项目时看到一个接口类型的变量调用了接口里的方法,底层逻辑跟我们刚才做练习时是完全一样的——程序在运行时动态决定由哪个实现类来干活。

6. 我自己在真正掌握多态之后的一点体会

最后多说几句不该写在教科书里的话。刚学多态那阵子,我老有一种“用上了多态就代表我很强”的错觉,动不动就抽象出一个父类,哪怕只有一个子类也要建一层抽象。后来写的东西多了,才慢慢明白多态真正值钱的地方不在于“用”它,而在于“什么时候不用它”。

一个经验是:抽象层是用来对抗变化的,不是用来表演优雅的。如果你的代码只有两个类,而且看不到新增类型的可能,那直接写具体类比硬上多态舒服得多。反过来,如果你的业务明确要扩展一套“同类不同实现”的组件,比如报表导出、消息推送、支付渠道,那么多态能帮你把扩展成本降到非常低。

第二个经验是:代码写完之后,每隔一段时间可以回头看一眼自己写的类结构。如果一个抽象类下面只有一两个子类,而且抽象方法常年没有被新实现覆盖,那这层抽象十有八九是多余的,可以大胆地折叠掉。真正合理的抽象是“加新功能时你根本不用思考就能找到插入点”,而不是“为了让代码显得灵活专门建一堆关系”。

第三个经验跟阅读有关。当你看到别人的代码里出现抽象类引用、接口引用时,不要急着跳进去看实现。先看业务调用方做了什么,再看抽象层提供了什么能力,最后才按需点开具体实现。只要调用方那层逻辑足够清晰,多态带给你的就是一种“阅读上的舒适”——你不需要关心细节也基本能猜到流程,因为抽象方法的名字已经把意图写在脸上了。

这一篇没有新的语法要背,但你掌握的东西会在后面学集合、学框架时反复出现。如果你能独立把练习里的代码写出来,并且想清楚第5节那几道思考题,那“多态”这一关就真的过了。下一篇我们继续往前,Java这个世界的“接口”篇章已经在向你招手了。

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

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

立即咨询