从真实开发任务看懂重载与重写
很多初学者学 "重载""重写" 时,总觉得是枯燥的语法规则,到了实际开发中踩了坑后发现:这两种技术根本不是 "语法糖",而是解决业务痛点的实用工具。接下来我们从电商项目的两个真实开发任务入手,看重载与重写如何从业务需求里自然 "长" 出来。
场景一:商品库存查询工具类 ------ 为什么需要 "重载"?
周二下午的需求评审会,产品经理张姐把后端开发小李叫到工位:"运营同事需要一个库存查询工具,得支持三种场景:一是输个商品 ID 查单条库存;二是选分类 ID + 最小库存阈值,查该分类下库存充足的商品;三是按商品状态(在售 / 下架)查对应库存列表。这周得交付,后续运营要高频用。"
最初的 "糟糕方案":方法名混乱成灾
小李没多想,按 "一个需求一个方法" 的思路写了代码:
kotlin
// 商品库存查询工具类(初始版本)
public class StockQueryUtil {
// 1. 根据商品ID查单个库存
public Stock getStockByProductId(Long productId) {
// 查数据库:select * from stock where product_id = ?
return stock;
}
// 2. 根据分类ID+最小库存查列表
public List<Stock> getStockByCategoryAndMin(Long categoryId, Integer minStock) {
// 查数据库:select * from stock where category_id = ? and stock_num >= ?
return stockList;
}
// 3. 根据商品状态查列表
public List<Stock> getStockByStatus(Integer status) {
// 查数据库:select * from stock where product_status = ?
return stockList;
}
}
结果第二天,前端开发小王就来找小李:"你这方法名也太绕了吧?查单个库存是 getStockByProductId 还是 getStockById?查分类的是 getStockListByCategory 还是 getStockByCategoryAndMin?我每次调用都要翻你文档,太影响效率了!"
小李自己也意识到问题:这三个功能本质都是 "查库存",只是查询维度不同,却要记三个长得像又不一样的方法名 ------ 不仅调用方麻烦,后续自己维护时,也容易忘了哪个方法对应哪个场景。
优化方案:用 "重载" 统一方法名
这时小李突然想起大学学过的 "重载":同一类中,方法名相同但参数不同,就能实现 "同一功能,多场景适配"。他立刻重构了代码:
kotlin
// 商品库存查询工具类(重载优化版)
public class StockQueryUtil {
// 1. 根据商品ID查单个库存(参数:商品ID)
public Stock getStock(Long productId) {
// 查数据库逻辑不变
return stock;
}
// 2. 根据分类ID+最小库存查列表(参数:分类ID+最小库存)
public List<Stock> getStock(Long categoryId, Integer minStock) {
// 查数据库逻辑不变
return stockList;
}
// 3. 根据商品状态查列表(参数:商品状态)
public List<Stock> getStock(Integer status) {
// 查数据库逻辑不变
return stockList;
}
}
重构后,小王调用时直接写getStock(123L)(查单个)、getStock(45L, 100)(查分类 + 阈值)、getStock(1)(查在售状态)------ 不用记复杂后缀,看参数就知道对应哪个场景,效率直接提上来了。
从场景里 "长" 出重载概念
其实这就是重载的核心:同一类中,为 "语义相同的功能" 定义统一方法名,靠 "参数列表差异"(参数个数、类型、顺序不同)区分不同实现。比如 "查库存" 这个语义,不管是按 ID、按分类 + 阈值,还是按状态,都用getStock(),让代码更贴合 "人类理解习惯"。
这里要注意两个关键规则:
- 重载的判断标准只有 "方法名相同 + 参数列表不同",返回值、访问权限不影响(比如上面方法返回Stock或List都可以);
- 参数列表差异必须是 "实质性的"(比如getStock(Long)和getStock(Integer)算不同,因为参数类型不同;但getStock(Long)和getStock(Long id)不算,因为只是参数名不同)。
场景二:商品库存扣减接口 ------ 为什么需要 "重写"?
过了一周,张姐又找小李:"现在商品要分'实体商品'和'电子商品'了!实体商品扣库存要关联仓库(比如减货架数量),电子商品扣库存要生成兑换码(比如游戏点卡),但订单系统调用扣库存时,不想管是实体还是电子 ------ 他们只传商品 ID 和数量,剩下的逻辑得咱们封装好。而且下个月要加'预售商品',到时候别让订单系统改代码!"
最初的 "糟糕方案":if-else 堆成山
小李一开始想:简单,加个 "商品类型" 参数,用 if-else 判断不就行了?于是写了这样的代码:
arduino
// 库存扣减服务(初始版本)
public class StockDeductService {
public boolean deductStock(Long productId, int quantity, String productType) {
if ("PHYSICAL".equals(productType)) {
// 实体商品逻辑:减仓库库存,生成出库记录
updateWarehouseStock(productId, quantity);
createOutboundRecord(productId, quantity);
} else if ("ELECTRONIC".equals(productType)) {
// 电子商品逻辑:减虚拟库存,生成兑换码
updateVirtualStock(productId, quantity);
generateRedeemCode(productId, quantity);
} else {
throw new IllegalArgumentException("未知商品类型");
}
return true;
}
}
但写完他自己就慌了:下个月加 "预售商品",要再加个else if ("PRE_SALE".equals(productType));以后再加 "二手商品",又要加 ------ 这代码会越来越臃肿,而且每次加新类型,都要改deductStock()方法,违反了 "对扩展开放、对修改关闭" 的开发原则(开闭原则)。
优化方案:用 "重写" 实现多态适配
这时小李想到 "面向对象" 的核心思想:父类定义规则,子类实现细节。他重构了代码,引入 "重写":
java
// 1. 父类:定义统一的"扣库存"规则(抽象方法)
public abstract class Product {
// 抽象方法:只定义"扣库存"的入参和返回值,不写具体逻辑
public abstract boolean deductStock(int quantity);
}
// 2. 实体商品子类:重写父类方法,实现实体商品逻辑
public class PhysicalProduct extends Product {
private Long warehouseId; // 实体商品特有属性:仓库ID
@Override
public boolean deductStock(int quantity) {
// 实体商品专属逻辑:减仓库库存+生成出库记录
updateWarehouseStock(warehouseId, quantity);
createOutboundRecord(getProductId(), quantity);
return true;
}
}
// 3. 电子商品子类:重写父类方法,实现电子商品逻辑
public class ElectronicProduct extends Product {
@Override
public boolean deductStock(int quantity) {
// 电子商品专属逻辑:减虚拟库存+生成兑换码
updateVirtualStock(getProductId(), quantity);
generateRedeemCode(getProductId(), quantity);
return true;
}
}
// 4. 调用方(订单系统):只认父类,不管子类
public class OrderService {
// 商品工厂:根据商品ID获取对应子类对象(隐藏子类创建细节)
private Product getProduct(Long productId) {
String type = getProductTypeFromDb(productId); // 查数据库获取商品类型
if ("PHYSICAL".equals(type)) {
return new PhysicalProduct(productId, 1001L); // 1001是仓库ID
} else {
return new ElectronicProduct(productId);
}
}
// 订单处理:调用扣库存时,只操作父类引用
public void processOrder(Long productId, int quantity) {
Product product = getProduct(productId);
// 关键:不用判断商品类型,直接调用父类方法,自动执行子类逻辑
product.deductStock(quantity);
}
}
这样一来,订单系统调用deductStock()时,完全不用关心商品是实体还是电子 ------ 父类引用Product指向哪个子类对象,就自动执行哪个子类的deductStock()方法(这就是多态的落地)。
下个月加 "预售商品" 时,只需要新增一个PreSaleProduct子类,重写deductStock()方法,订单系统的代码一行都不用改:
scala
// 新增预售商品子类:无需修改原有代码
public class PreSaleProduct extends Product {
@Override
public boolean deductStock(int quantity) {
// 预售商品专属逻辑:减预售库存,更新发货时间
updatePreSaleStock(getProductId(), quantity);
updateDeliveryTime(getProductId());
return true;
}
}
从场景里 "长" 出重写概念
这就是重写的核心:父子类间,子类对父类的 "抽象方法 / 普通方法" 进行 "同名、同参数、同返回值" 的具体实现,目的是让 "同一行为在不同子类中有不同表现",最终支撑多态。
重写有三个关键规则:
- 方法签名必须完全一致(方法名、参数列表、返回值类型完全相同,子类返回值可是父类的子类,比如父类返回Product,子类可返回PhysicalProduct);
- 子类方法的访问权限不能低于父类(比如父类是public,子类不能是private);
- 父类如果是抽象方法,子类必须重写(除非子类也是抽象类)。
重载与重写:一张表分清核心区别
看到这里,可能有人会混淆 "重载" 和 "重写"------ 其实结合前面的场景,用一张表就能分清:
对比维度 | 重载(Overload) | 重写(Override) |
---|---|---|
适用场景 | 同一类中,"同一功能多场景" | 父子类间,"同一行为多实现" |
方法签名 | 方法名相同,参数列表不同 | 方法名、参数列表、返回值完全相同 |
核心目的 | 简化调用(不用记多个方法名) | 实现多态(提高代码扩展性) |
调用时的区别 | 编译期确定调用哪个方法(静态绑定) | 运行期确定调用哪个方法(动态绑定) |
示例对应 | getStock(Long)/getStock(Integer) | PhysicalProduct.deductStock()/ElectronicProduct.deductStock() |
最后:技术永远服务于业务
很多人学编程时,会陷入 "死记语法规则" 的误区,但从上面的真实场景能看出:重载不是 "为了同名而同名",而是为了解决 "方法名混乱" 的业务痛点;重写也不是 "为了继承而重写",而是为了解决 "代码扩展性差" 的业务难题。
记住:开发的本质是用技术解决业务问题。当你下次纠结 "要不要用重载 / 重写" 时,不妨回到业务场景里想一想:这样写能不能让调用更简单?能不能让后续加需求时少改代码?想清楚这两个问题,技术选型自然就清晰了。