自动售货机纸币硬币找零逻辑问题与代码调试求助
自动售货机找零逻辑修复方案
问题根源
原Money类仅通过总金额处理加减操作,完全忽略了纸币/硬币的具体面额组成。比如单张10美元纸币的场景下,调用TenDollar.minus(OneDollar)时,系统只计算总金额差值为9美元,却没意识到根本没有1美元的零钱可以找,导致本该抛出的IllegalArgumentException没触发。
解决思路
- 重构状态存储:放弃单一总金额的存储方式,改用
Map<Denomination, Integer>记录每种面额的纸币/硬币数量,Denomination作为枚举类定义所有支持的面额(如1元、5元、10元)。 - 重写找零逻辑:
- 处理找零请求时,先检查是否能直接用对应面额的零钱抵扣;
- 若无法直接抵扣,尝试用现有面额组合出所需找零金额;
- 若两种方式都无法满足,立即抛出异常;
- 找零成功则返回扣除后的剩余金额实例,保证原实例不可变。
代码实现
1. 定义面额枚举
public enum Denomination { ONE_DOLLAR(1), FIVE_DOLLAR(5), TEN_DOLLAR(10); private final int value; Denomination(int value) { this.value = value; } public int getValue() { return value; } }
2. 重构后的Money类
import java.util.HashMap; import java.util.Map; public class Money { private final Map<Denomination, Integer> denominationCounts; public Money() { this.denominationCounts = new HashMap<>(); } // 快速创建单一面额的Money实例 public static Money of(Denomination denom) { Money money = new Money(); money.denominationCounts.put(denom, 1); return money; } // 计算总金额 public int getTotal() { return denominationCounts.entrySet().stream() .mapToInt(entry -> entry.getKey().getValue() * entry.getValue()) .sum(); } // 找零操作:返回剩余金额,无法找零抛IllegalArgumentException public Money minus(Money amountToSubtract) { Map<Denomination, Integer> remaining = new HashMap<>(this.denominationCounts); for (Map.Entry<Denomination, Integer> entry : amountToSubtract.denominationCounts.entrySet()) { Denomination targetDenom = entry.getKey(); int requiredCount = entry.getValue(); int requiredTotal = targetDenom.getValue() * requiredCount; // 先尝试直接扣除对应面额 int available = remaining.getOrDefault(targetDenom, 0); if (available >= requiredCount) { remaining.put(targetDenom, available - requiredCount); if (remaining.get(targetDenom) == 0) { remaining.remove(targetDenom); } continue; } // 直接扣除不行,尝试组合凑钱 if (!canMakeChange(remaining, requiredTotal)) { throw new IllegalArgumentException("无法完成找零"); } } Money result = new Money(); result.denominationCounts.putAll(remaining); return result; } // 辅助方法:检查能否用现有面额凑出目标金额,凑成后更新可用面额数量 private boolean canMakeChange(Map<Denomination, Integer> available, int target) { // 从大面额到小面额尝试,优先用大面额凑钱 for (Denomination denom : Denomination.values()) { int denomValue = denom.getValue(); if (denomValue > target) continue; int maxPossible = Math.min(available.getOrDefault(denom, 0), target / denomValue); for (int useCount = maxPossible; useCount >= 0; useCount--) { int newTarget = target - useCount * denomValue; if (newTarget == 0) { // 凑成了,更新可用数量 updateAvailableCount(available, denom, useCount); return true; } // 递归尝试小面额 if (canMakeChange(available, newTarget)) { updateAvailableCount(available, denom, useCount); return true; } } } return false; } // 辅助方法:更新可用面额数量,数量为0则移除 private void updateAvailableCount(Map<Denomination, Integer> available, Denomination denom, int useCount) { int newCount = available.getOrDefault(denom, 0) - useCount; if (newCount == 0) { available.remove(denom); } else { available.put(denom, newCount); } } // 测试用静态实例 public static final Money OneDollar = Money.of(Denomination.ONE_DOLLAR); public static final Money TenDollar = Money.of(Denomination.TEN_DOLLAR); }
3. 测试验证
import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; public class MoneyTest { @Test public void testCannotMakeChangeWithSingleTenDollar() { assertThrows(IllegalArgumentException.class, () -> Money.TenDollar.minus(Money.OneDollar)); } }
核心亮点
- 精准面额跟踪:通过映射记录每种纸币/硬币的数量,彻底解决总金额判断的盲区;
- 严谨找零逻辑:先直接扣减对应面额,再尝试组合凑钱,确保只有实际能拿出的零钱才会被用于找零;
- 不可变设计:操作时复制原面额数据,避免修改原始实例,符合值对象的设计规范。
内容的提问来源于stack exchange,提问作者Ghost.Coder
相关产品推荐
相关产品推荐

