You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自动售货机纸币硬币找零逻辑问题与代码调试求助

自动售货机找零逻辑修复方案

问题根源

原Money类仅通过总金额处理加减操作,完全忽略了纸币/硬币的具体面额组成。比如单张10美元纸币的场景下,调用TenDollar.minus(OneDollar)时,系统只计算总金额差值为9美元,却没意识到根本没有1美元的零钱可以找,导致本该抛出的IllegalArgumentException没触发。

解决思路

  1. 重构状态存储:放弃单一总金额的存储方式,改用Map<Denomination, Integer>记录每种面额的纸币/硬币数量,Denomination作为枚举类定义所有支持的面额(如1元、5元、10元)。
  2. 重写找零逻辑:
    • 处理找零请求时,先检查是否能直接用对应面额的零钱抵扣;
    • 若无法直接抵扣,尝试用现有面额组合出所需找零金额;
    • 若两种方式都无法满足,立即抛出异常;
    • 找零成功则返回扣除后的剩余金额实例,保证原实例不可变。

代码实现

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 13:32:19