coding……
但行好事 莫问前程

Java编程拾遗『EnumMap』

到本文为止,之前已经介绍过HashMap、TreeMap、LinkedHashMap,本篇文章介绍一下集合框架系列最后一种类型的Map——EnumMap。在之前的文章中,我曾介绍过Java API中枚举类的本质(Java编程拾遗『枚举类』),本文讲的EnumMap就是一种key为枚举类型的Map结构。EnumMap类的继承关系如下:

Enum类继承了AbstractMap抽象类,在EnumMap对相应方法做了特别的实现,保证EnumMap key为特定枚举类型,键值对按照枚举类定义的顺序有序

1. 调用示例

public enum SizeEnum {
    SMALL, MEDIUM, LARGE, EXTRA_LARGE
}
@AllArgsConstructor
@Getter
@Setter
public class Clothe {
    Long id;
    SizeEnum size;
}
public class EnumMapTest {
    public static void main(String[] args) {
        List<Clothe> clothes = Lists.newArrayList();
        clothes.add(new Clothe(1L, SizeEnum.LARGE));
        clothes.add(new Clothe(2L, SizeEnum.EXTRA_LARGE));
        clothes.add(new Clothe(3L, SizeEnum.SMALL));
        clothes.add(new Clothe(4L, SizeEnum.SMALL));

        Map<SizeEnum, Integer> map = new EnumMap<>(SizeEnum.class);
        for (Clothe c : clothes) {
            map.compute(c.getSize(), (k, v) -> (v == null) ? 1 : ++v);
        }

        System.out.println(map);
    }
}

统计Clothe List中,各个Size的数目,运行结果如下:

{SMALL=2, LARGE=1, EXTRA_LARGE=1}

可以看到,EnumMap各键值对之间的顺序跟枚举类定义的顺序一致。

2. 方法说明

2.1 构造函数

S.N. 方法 说明
1 public EnumMap(Class<K> keyType) 声明key类型为keyType的EnumMap
2 public EnumMap(EnumMap<K, ? extends V> m) 通过EnumMap对象初始化一个EnumMap对象
3 public EnumMap(Map<K, ? extends V> m) 通过Map对象初始化一个EnumMap对象,如果Map非EnumMap,则要求key类型为Enum类

2.2 Map接口

除了构造函数之外的方法,全部继承自Map接口,如下:

S.N. 方法 说明
1 void clear() 清除Map中所有的KV映射
2 default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) Java 8新方法,如果key在Map中不存在,则根据Function规则计算key对应的value值,
并进行put操作(前提计算得到的value值也不为null)
3 default V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction)
Java 8新方法,如果key在Map中存在,则根据BiFunction规则通过key和对应的value
计算newValue,如果newValue为null,则删除key对应的键值对,否则替换key对应的value值
4 default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) Java 8新方法,相当于上述两个方法的综合,根据BiFunction,通过key和对应的value
计算newValue,如果newValue为null,会删除对应KV映射,否则会插入或替换原KV映射
5 boolean containsKey(Object key) 判断Map中是否存在key键
6 boolean containsValue(Object value) 判断Map中是否存在value值
7 Set<Map.Entry<K, V>> entrySet() 将Map所有的键值对转换为Set集合
8 boolean equals(Object o) equals方法
9 default void forEach(BiConsumer<? super K, ? super V> action) Java 8新方法,遍历Map,通过BiConsumer处理Map中每一个键值对的key、value
10 V get(Object key) 获取Map中key对应的value
11 default V getOrDefault(Object key, V defaultValue) Java 8新方法,获取Map中key对应的value,如果key不存在,则返回defaultValue
12 int hashCode() hashCode方法
13 boolean isEmpty() 判断Map是否为空
14 Set<K> keySet() 将Map中所有的key转化为Set集合
15 default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction)
如果key在Map中不存在或者对应的值为null,则给其一个对应的非null值value。否则根据
BiFunction规则通过oldValue和value计算一个newValue,替换或删除(newValue为null)原
键值对
16 V put(K key, V value); 为Map添加一个键值对
17 void putAll(Map<? extends K, ? extends V> m) 将另一个Map的所有键值对添加到该Map中
18 default V putIfAbsent(K key, V value) Java 8新方法,如果Map中key不存在,则添加一个key-value键值对
19 V remove(Object key) 从Map中移除key对应的键值对
20 default boolean remove(Object key, Object value) Java8新方法,删除key-value键值对
21 default V replace(K key, V value) Java8新方法,将Map中key对应的值替换为value
22 default boolean replace(K key, V oldValue, V newValue) Java8新方法,将Map中key-oldValue键值对的value值替换为newValue
23 default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) Java8新方法,将Map中所有的键值对的value,替换为根据BiFunction规则通过key-value
得到的新值
24 int size() 返回Map中EntrySet的数目
25 Collection<V> values() 返回Map中所有value组成的集合

3. 实现源码分析

3.1 EnumMap类定义

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{
    private final Class<K> keyType;

    private transient K[] keyUniverse;

    private transient Object[] vals;

    private transient int size = 0;

    //methods

}
  • keyType:EnumMap key对应的枚举类型
  • keyUniverse:EnumMap key对应的枚举类型的所有枚举值组成的数组,顺序跟枚举类中枚举值定义顺序一致
  • vals:EnumMap中所有key对应的value值组成的数组,顺序跟枚举类中枚举值定义顺序一致
  • size:EnumMap中有效键值对数目

比如上面示例中,EnumMap为:

{SMALL=2, LARGE=1, EXTRA_LARGE=1}

底层存储如下图所示:

3.2 构造函数

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

构造函数,传入EnumMap key类型,构造函数中完成将传入的keyType赋值给EnumMap成员变量keyType,调用getKeyUniverse方法,将keyType对应的枚举类的所有枚举值组成的数组赋值给keyUniverse,vals初始化为跟枚举值数量相同的Object数组。

public EnumMap(EnumMap<K, ? extends V> m) {
    keyType = m.keyType;
    keyUniverse = m.keyUniverse;
    vals = m.vals.clone();
    size = m.size;
}

通过EnumMap对象构造EnumMap,通过对成员变量直接赋值实现。

public EnumMap(Map<K, ? extends V> m) {
    if (m instanceof EnumMap) {
        EnumMap<K, ? extends V> em = (EnumMap<K, ? extends V>) m;
        keyType = em.keyType;
        keyUniverse = em.keyUniverse;
        vals = em.vals.clone();
        size = em.size;
    } else {
        if (m.isEmpty())
            throw new IllegalArgumentException("Specified map is empty");
        keyType = m.keySet().iterator().next().getDeclaringClass();
        keyUniverse = getKeyUniverse(keyType);
        vals = new Object[keyUniverse.length];
        putAll(m);
    }
}

通过Map对象构造EnumMap对象,这里分为两种情况,如果Map对象是一个EnumMap示例,则跟上面的构造函数实现一样。否则,则要求Map的key类型为枚举类型,通过Map的key获取keyType,通过上面构造函数1,初始化EnumMap,然后调用putAll方法,将Map中所有的键值对添加到EnumMap中。

3.3 put方法

public V put(K key, V value) {
    //检查待插入key类型是否为keyType
    typeCheck(key);

    //获取枚举值key在枚举类中对应的ordinal(序号)
    int index = key.ordinal();
    //取key在value数组中对应的值
    Object oldValue = vals[index];
    //将新的value写入value数组对应index位置
    vals[index] = maskNull(value);
    //如果oldValue为null,说明key在EnumMap中不存在,将size自增1
    if (oldValue == null)
        size++;
    //返回oldValue值
    return unmaskNull(oldValue);
}

maskNull和unmaskNull方法用于对null进行包装和反包装。可以发现,vals数组中null元素只有可能来自构造函数,一旦进行put操作,即使带插入的键值对value为null,插入之后,vals对应index位置也不是null了

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

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

NULL为一个静态的Object对象,如下:

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

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

3.4 remove方法

public V remove(Object key) {
    //检查待插入key类型是否为keyType
    if (!isValidKey(key))
        return null;

    //获取枚举值key在枚举类中对应的ordinal(序号)
    int index = ((Enum<?>)key).ordinal();
    //获取key在value数组中对应的值
    Object oldValue = vals[index];
    //将index索引位置的value元素
    vals[index] = null;
    //如果oldValue不为null,说明index索引位置之前插入过元素,现在做remove操作后,将size自减
    if (oldValue != null)
        size--;

    //对oldValue反包装
    return unmaskNull(oldValue);
}

3.4 get方法

//获取EnumMap中键key对应的value
//如果key是EnumMap中合法的key,则返回vals数组中对应index的值,否则返回null
public V get(Object key) {
    return (isValidKey(key) ?
            unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}

isValidKey方法如下:

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;
}

3.5 containsKey方法

//判断key是否存在,条件:key合法 && vals数组对应位置不为null
public boolean containsKey(Object key) {
    return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
}

3.6 containsValue方法

//判断value是否存在
public boolean containsValue(Object value) {
    //对value进行包装
    value = maskNull(value);

    //遍历vals数组,判断value是否存在
    for (Object val : vals)
        if (value.equals(val))
            return true;

    return false;
}

4. EnumMap特点

EnumMap的key为枚举类型,为了便于更好的理解EnumMap,搞清楚枚举类的本质还是很重要的(Java编程拾遗『枚举类』)。

  • EnumMap内部有两个数组,长度相同,分别用来表示key对应的枚举类所有的枚举值,和所有key对应的value,由于枚举类的特性,根据枚举值可以获取枚举值对应的ordinal,进而获取key和value在EnumMap内部数组的存储位置,所以效率很高,get、put、remove、containsKey等操作时间复杂度都为n(1)
  • EnumMap可以保证键值对之间按照枚举值有序

参考链接:

  1. Java API源码

 

赞(1) 打赏
Zhuoli's Blog » Java编程拾遗『EnumMap』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址