剖析EnumMap

如果需要一个Map的实现类,并且键的类型为枚举类型,可以使用HashMap,但应该使用一个专门的实现类EnumMap。

为什么要有一个专门的类呢?我们之前介绍过枚举的本质,主要是因为枚举类型有两个特征,一是它可能的值是有限的且预先定义的,二是枚举值都有一个顺序,这两个特征使得可以更为高效的实现Map接口。

我们先来看EnumMap的用法,然后看它到底是怎么实现的。

用法

举个简单的例子,比如,有一批关于衣服的记录,我们希望按尺寸统计衣服的数量。

定义一个简单的枚举类,Size,表示衣服的尺寸:

public enum Size {
    SMALL, MEDIUM, LARGE
}

定义一个简单类,Clothes,表示衣服:

class Clothes {
    String id;
    Size size;
    
    public Clothes(String id, Size size) {
        this.id = id;
        this.size = size;
    }

    public String getId() {
        return id;
    }

    public Size getSize() {
        return size;
    }
}

有一个表示衣服记录的列表List<Clothes>,我们希望按尺寸统计数量,统计方法可以为:

public static Map<Size, Integer> countBySize(List<Clothes> clothes){
    Map<Size, Integer> map = new EnumMap<>(Size.class);
    for(Clothes c : clothes){
        Size size = c.getSize();
        Integer count = map.get(size);
        if(count!=null){
            map.put(size, count+1);
        }else{
            map.put(size, 1);
        }
    }
    return map;
}

大部分代码都很简单,需要注意的是EnumMap的构造方法,如下所示:

Map<Size, Integer> map = new EnumMap<>(Size.class);

与HashMap不同,它需要传递一个类型信息,我们在37节简单介绍过运行时类型信息,Size.class表示枚举类Size的运行时类型信息,Size.class也是一个对象,它的类型是Class。

为什么需要这个参数呢?没有这个,EnumMap就不知道具体的枚举类是什么,也无法初始化内部的数据结构。

使用以上的统计方法也是很简单的,比如:

List<Clothes> clothes = Arrays.asList(new Clothes[]{
        new Clothes("C001",Size.SMALL),
        new Clothes("C002", Size.LARGE),
        new Clothes("C003", Size.LARGE),
        new Clothes("C004", Size.MEDIUM),
        new Clothes("C005", Size.SMALL),
        new Clothes("C006", Size.SMALL),
});
System.out.println(countBySize(clothes));

输出为:

{SMALL=3, MEDIUM=1, LARGE=2}

需要说明的是,EnumMap是保证顺序的,输出是按照键在枚举中的顺序的。

除了以上介绍的构造方法,EnumMap还有两个构造方法,可以接受一个键值匹配的EnumMap或普通Map,如下所示:

public EnumMap(EnumMap<K, ? extends V> m)
public EnumMap(Map<K, ? extends V> m)

比如:

Map<Size,Integer> hashMap = new HashMap<>();
hashMap.put(Size.LARGE, 2);
hashMap.put(Size.SMALL, 1);
Map<Size, Integer> enumMap = new EnumMap<>(hashMap);

以上就是EnumMap的基本用法,与HashMap的主要不同,一是构造方法需要传递类型参数,二是保证顺序。

有人可能认为,对于枚举,使用Map是没有必要的,比如对于上面的统计例子,可以使用一个简单的数组:

public static int[] countBySize(List<Clothes> clothes){
    int[] stat = new int[Size.values().length];
    for(Clothes c : clothes){
        Size size = c.getSize();
        stat[size.ordinal()]++;
    }
    return stat;
}

这个方法可以这么使用:

List<Clothes> clothes = Arrays.asList(new Clothes[]{
        new Clothes("C001",Size.SMALL),
        new Clothes("C002", Size.LARGE),
        new Clothes("C003", Size.LARGE),
        new Clothes("C004", Size.MEDIUM),
        new Clothes("C005", Size.SMALL),
        new Clothes("C006", Size.SMALL),
});
int[] stat = countBySize(clothes);
for(int i=0; i<stat.length; i++){
    System.out.println(Size.values()[i]+": "+ stat[i]);
}

输出为:

SMALL 3
MEDIUM 1
LARGE 2

可以达到同样的目的。但,直接使用数组需要自己维护数组索引和枚举值之间的关系,正如枚举的优点是简洁、安全、方便一样,EnumMap同样是更为简洁、安全、方便,它内部也是基于数组实现的,但隐藏了细节,提供了更为方便安全的接口。

实现原理

下面我们来看下具体的代码,从内部组成开始。

内部组成

EnumMap有如下实例变量:

private final Class<K> keyType;
private transient K[] keyUniverse;
private transient Object[] vals;
private transient int size = 0;

keyType表示类型信息,keyUniverse表示键,是所有可能的枚举值,vals表示键对应的值,size表示键值对个数。

构造方法

EnumMap的基本构造方法代码为:

public EnumMap(Class<K> keyType) {
    this.keyType = keyType;
    keyUniverse = getKeyUniverse(keyType);
    vals = new Object[keyUniverse.length];
}

调用了getKeyUniverse以初始化键数组,其代码为:

private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
    return SharedSecrets.getJavaLangAccess()
                                    .getEnumConstantsShared(keyType);
}

这段代码又调用了其他一些比较底层的代码,就不列举了,原理是最终调用了枚举类型的values方法,values方法返回所有可能的枚举值。关于values方法,我们在枚举的本质一节介绍过其用法和实现原理,这里就不赘述了。

保存键值对

put方法的代码为:

public V put(K key, V value) {
    typeCheck(key);

    int index = key.ordinal();
    Object oldValue = vals[index];
    vals[index] = maskNull(value);
    if (oldValue == null)
        size++;
    return unmaskNull(oldValue);
}

首先调用typeCheck检查键的类型,其代码为:

private void typeCheck(K key) {
    Class keyClass = key.getClass();
    if (keyClass != keyType && keyClass.getSuperclass() != keyType)
        throw new ClassCastException(keyClass + " != " + keyType);
}

如果类型不对,会抛出异常。类型正确的话,调用ordinal获取索引index,并将值value放入值数组vals[index]中。EnumMap允许值为null,为了区别null值与没有值,EnumMap将null值包装成了一个特殊的对象,有两个辅助方法用于null的打包和解包,打包方法为maskNull,解包方法为unmaskNull。这个特殊对象及两个方法的代码为:

private static final Object NULL = new Object() {
    public int hashCode() {
        return 0;
    }

    public String toString() {
        return "java.util.EnumMap.NULL";
    }
};

private Object maskNull(Object value) {
    return (value == null ? NULL : value);
}

private V unmaskNull(Object value) {
    return (V) (value == NULL ? null : value);
}

根据键获取值

get方法的代码为:

public V get(Object key) {
    return (isValidKey(key) ?
            unmaskNull(vals[((Enum)key).ordinal()]) : null);
}

键有效的话,通过ordinal方法取索引,然后直接在值数组vals里找。isValidKey的代码与typeCheck类似,但是返回boolean值而不是抛出异常,代码为:

private boolean isValidKey(Object key) {
    if (key == null)
        return false;

    // Cheaper than instanceof Enum followed by getDeclaringClass
    Class keyClass = key.getClass();
    return keyClass == keyType || keyClass.getSuperclass() == keyType;
}

查看是否包含某个值

containsValue方法的代码为:

public boolean containsValue(Object value) {
    value = maskNull(value);

    for (Object val : vals)
        if (value.equals(val))
            return true;

    return false;
}

<think>嗯,用户问的问题是关于Java代码中一个DiscountCalculator类的,特别是当里面的EnumMap是空的时候会发生什么。看起来用户可能是在处理折扣计算逻辑时遇到了问题,或者在测试时发现当没有应用任何折扣时结果不符合预期。我需要先理解这段代码的结构和功能。 首先,DiscountCalculator类有一个activeDiscounts,这是一个EnumMap,键是DiscountType.DiscountGroup,值是DiscountType。EnumMap的特点是键必须是枚举类型,而且效率比较高。applyDiscount方法用来添加或替换折扣,而calculateFinalPrice则是遍历所有的折扣,将原价依次乘以各个折扣率,得到最终价格。 用户的问题是,如果activeDiscounts为空,也就是没有应用任何折扣,那会怎么样?这时候calculateFinalPrice方法会如何处理呢?根据代码逻辑,当activeDiscounts.values()为空时,循环不会执行,直接返回originalPrice。因此,最终价格应该等于原始价格,没有任何折扣。但用户可能需要确认这点,或者想知道是否有潜在的问题,比如返回null或者异常。 接下来需要考虑用户可能的实际使用场景。例如,在电商系统中,用户可能有不同的折扣分组,比如会员折扣、促销折扣等。当没有应用任何折扣时,系统应该正常返回原价。但用户可能担心如果activeDiscounts为空,代码是否会出错,或者是否有默认处理逻辑。 另外,用户可能没有考虑到折扣叠加的情况,比如多个折扣是否应该应用,或者是否需要有一个默认的折扣率(比如1.0)来确保即使没有折扣,计算也能正确进行。不过根据当前代码,如果没有任何折扣,循环不执行,直接返回原价是正确的。 还需要检查是否有异常情况,比如如果activeDiscounts为null,但这里activeDiscounts被初始化为一个空的EnumMap,所以不会为null,只是empty。因此循环处理时不会有NullPointerException。 用户可能还关心如何测试这种情况,或者是否需要添加日志或断言来确保当没有折扣时原价被正确返回。或者是否需要修改代码,比如在没有折扣时添加一个默认的折扣率,但根据当前设计,可能不需要。 总结来说,当activeDiscounts为空时,calculateFinalPrice返回原价是正确的,不会有错误。但需要确认用户的需求是否允许这种情况,或者是否应该强制至少有一个折扣。如果用户希望必须有至少一个折扣,那么代码中可能需要添加校验,抛出异常。否则,当前的处理是合理的。</think>### **分步解析与解决方案** --- #### **一、代码行为分析** ```java public BigDecimal calculateFinalPrice(BigDecimal originalPrice) { BigDecimal result = originalPrice; for (DiscountType discount : activeDiscounts.values()) { result = result.multiply(discount.getRate()); } return result; } ``` **关键逻辑**: 1. 初始化`result`为原始价格 2. 遍历所有已激活的折扣(`activeDiscounts.values()`) 3. 连续乘以每个折扣的比率(`discount.getRate()`) --- #### **二、空Map场景推演** 当`activeDiscounts`为空时(即未调用过`applyDiscount`方法): - **循环条件**:`activeDiscounts.values()`返回空集合 - **执行结果**:循环体**不会执行** - **最终返回值**:`result`保持初始值`originalPrice` **数学表达式**: $$ \text{最终价格} = \text{原价} \times \prod_{i=1}^{n} \text{折扣率}_i \quad \text{当} \ n=0 \ \text{时结果为原价} $$ --- #### **三、风险预警** 虽然代码能正常运行,但需注意以下潜在问题: | 问题类型 | 具体表现 | 严重性 | |----------------|--------------------------------------------------------------------------|--------| | **业务逻辑** | 用户预期"至少有一个默认折扣",但实际无折扣时价格不变 | ★★★★☆ | | **数值计算** | 原价未被修改可能导致审计问题(例如需要记录是否应用过折扣) | ★★★☆☆ | | **代码健壮性** | 若后续开发者在循环后添加其他计算逻辑,可能忘记处理空集合场景 | ★★☆☆☆ | --- #### **四、解决方案对比** **方案1:保持当前逻辑** ```java // 适合场景:允许无折扣状态存在 return originalPrice; ``` **优点**: - 符合"没有折扣即原价"的直观认知 - 代码简洁 **缺点**: - 缺乏明确的业务状态标识 --- **方案2:添加默认折扣** ```java // 在构造器中初始化默认折扣 public DiscountCalculator() { activeDiscounts.put(DiscountGroup.DEFAULT, DiscountType.NONE); } ``` **数学表达**: $$ \text{默认折扣率} = 1.0 \implies \text{原价} \times 1.0 = \text{原价} $$ **优点**: - 确保计算流水线始终有值 - 便于扩展默认逻辑 **缺点**: - 可能不符合"显式无折扣"的业务需求 --- **方案3:空值校验(推荐)** ```java public BigDecimal calculateFinalPrice(BigDecimal originalPrice) { if (activeDiscounts.isEmpty()) { // 可选:记录日志、抛出异常或返回特定值 logger.warn("未应用任何折扣"); return originalPrice; } // 原有计算逻辑... } ``` **优势对比表**: | 校验方式 | 适用场景 | 系统影响 | |------------------|-----------------------------------|--------------------------| | 日志警告 | 需要监控无折扣情况 | 低(仅记录) | | 抛出异常 | 强制要求必须有折扣 | 高(中断流程) | | 返回特定标识值 | 需要特殊处理无折扣场景 | 中(需调用方配合) | --- #### **五、线程安全增强建议** 当前实现存在**复合操作风险**: ```java // 非原子操作示例 if (!calculator.activeDiscounts.isEmpty()) { // 检查 calculator.calculateFinalPrice(price); // 使用 } ``` **改进方案**: ```java public synchronized BigDecimal calculateFinalPrice(BigDecimal originalPrice) { // 方法体加锁保证原子性 } ``` --- #### **六、单元测试用例** ```java @Test void testEmptyDiscount() { DiscountCalculator calc = new DiscountCalculator(); BigDecimal original = new BigDecimal("100.00"); // 验证空折扣时返回原价 assertEquals(0, original.compareTo(calc.calculateFinalPrice(original))); // 验证浮点精度(推荐使用Comparator) assertTrue(original.compareTo(calc.calculateFinalPrice(original)) == 0); } ``` --- ### **最终结论** 当`activeDiscounts`为空时: 1. **代码层面**:安全返回原价,**不会报错** 2. **业务层面**:需根据具体需求选择处理策略: - 电商系统:允许无折扣(当前逻辑合理) - 金融系统:强制必须有折扣(应添加校验) 3. **最佳实践**:建议增加空集合校验逻辑,至少记录日志以便监控 **修改建议代码**: ```java public BigDecimal calculateFinalPrice(BigDecimal originalPrice) { Objects.requireNonNull(originalPrice); if (activeDiscounts.isEmpty()) { logger.debug("未应用折扣策略,返回基准价格"); return originalPrice.setScale(2, RoundingMode.HALF_UP); } BigDecimal result = originalPrice; for (DiscountType discount : activeDiscounts.values()) { result = result.multiply(discount.getRate()) .setScale(2, RoundingMode.HALF_UP); } return result; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值