Java 文档 - EnumMap/EnumSet

290 阅读7分钟

简介

一般来说我们会选择使用HashMap来存储key-value格式的数据,考虑这样的特殊情况,一个HashMap的key都来自于一个Enum类,这样的情况则可以考虑使用本文要讲的EnumMap。

EnumMap

先看一下EnumMap的定义和HashMap定义的比较:

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 

我们可以看到EnumMap几乎和HashMap是一样的,区别在于EnumMap的key是一个Enum。EnumMap是专门为枚举类型量身定做的Map实现。

下面看一个简单的使用的例子:

先定义一个Enum:

public enum Types {
    RED, GREEN, BLACK, YELLO
}

再看下怎么使用EnumMap:

    @Test
    public void useEnumMap(){
        EnumMap<Types, String> activityMap = new EnumMap<>(Types.class);
        activityMap.put(Types.BLACK,"black");
        activityMap.put(Types.GREEN,"green");
        activityMap.put(Types.RED,"red");
    }

EnumMap 源码

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable {

    // 枚举类的class对象,例如示例的Color.class
    private final Class<K> keyType;

    // 存储key的数组,key即枚举类的值对象,包括了name和ordinal两个属性
    private transient K[] keyUniverse;

    // 存储value的数组,value允许null,null会被转换成Object NULL实例替代存储
    private transient Object[] vals;

    private transient int size = 0;

    // Object的实例,用于代表null,用于区分值数组中元素本身是null(还未存值),
    // 还是存储的就是null值(已经存值)
    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);
    }

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

    // 其他省略
}

EnumMap的基本操作都比较快,都在常量时间内完成

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

private void typeCheck(K key) {
    Class<?> keyClass = key.getClass();
    if (keyClass != keyType && keyClass.getSuperclass() != keyType)
        throw new ClassCastException(keyClass + " != " + keyType);
}
步骤: 1、检查key的类型是否是枚举类的类型,否则抛出ClassCastException异常。 2、用key的ordinal(声明次序值)作为数组的索引下标。 3、将value存到数组key的下标里面。如果是null元素转换为NULL实例存储。 4、更新元素数量计数器size。

get()

public V get(Object key) {
    return (isValidKey(key) ?
            unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}
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;
}
步骤: 1、检查key的类型是否为相应枚举类的类型,key为null或者类型不匹配返回null。 2、用key的ordinal值作为数组的索引下标,查找元素并返回,如果为NULL实例,则转换为null后返回。

什么时候使用EnumMap

因为在EnumMap中,所有的key的可能值在创建的时候已经知道了,所以使用EnumMap和hashMap相比,可以提升效率。

同时,因为key比较简单,所以EnumMap在实现中,也不需要像HashMap那样考虑一些复杂的情况。

下面的代码给出了很清楚的解释:

private transient Object[] vals;
public V put(K key, V value) {
// ...
int index = key.ordinal();
vals[index] = maskNull(value);
// ...
}

上段代码的关键实现在于,我们用数组代替了哈希表。尤其是向map中插入新值时,所要做的仅仅是获得一个由编译器为每个枚举类型生成的常量序列号。如果有一个全局的map配置(例如只有一个实例),在增加访问速度的压力下,EnumMap 会获得比 HashMap 更加杰出的表现。原因在于 EnumMap 使用的堆内存比 HashMap 要少 一位(bit),而且 HashMap 要在每个键值上都要调用 hashCode() 方法和 equals() 方法。

EnumSet

跟EnumMap很类似,EnumSet是一个set,然后set中的元素都是某个Enum类型。

EnumSet是一个抽象类,要创建EnumSet类可以使用EnumSet提供的两个静态方法,noneOf和allOf。

EnumSet 源码

属性

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
    // 保存的数据类型
    final Class<E> elementType;
    // 保存数据的数组
    final Enum<?>[] universe;
}

我们通过继承体系和官网文档可以看出:

  • EnumSet是专门用于服务枚举类型的,内部存储是通过位向量来实现的,所以性能是很好的。
  • EnumSet和其他Set不太一样的地方,是它是有序的并且不允许插入null。
  • 不是线程同步的,如果要线程同步,可以使用Collections.synchronizedSet来同步;
  • 所有基本操作都在常量时间内执行。它们很可能(虽然不能保证)比HashSet还要快很多,如果它们的参数也是枚举,即使是批量操作也会在常量时间内执行。
  • 从JDK1.5开始引入。
  • 方法基本上都是静态方法。

构造方法

EnumSet(Class<E>elementType, Enum<?>[] universe) {
    this.elementType = elementType;
    this.universe    = universe;
}

EnumSet只有一个构造方法,不过该方法不是public的,不对外使用,只供子类继承时使用。

基本方法

noneOf()

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

noneOf传入一个Enum类,返回一个空的Enum类型的EnumSet。

从上面的代码我们可以看到EnumSet有两个实现,长度大于64的时候使用JumboEnumSet,小有64的时候使用RegularEnumSet。

注意,JumboEnumSet和RegularEnumSet不建议直接使用,他是内部使用的类。

allOf()

public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) {
        EnumSet<E> result = noneOf(elementType);
        result.addAll();
        return result;
    }
* allOf方法是创建一个包含指定类型的所有元素的EnumSet,通过源码可以看到,是先调用noneof方法创建一个空的Set,然后在调用EnumSet的addAll方法;* of方法有多个重载方法,参数从1到n,底层也是先调用noneof方法,再分n次调用EnumSet的add方法;

copyOf()

/**
 * 创建一个EnumSet,从指定集合拷贝数据
 */
public static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c) {
    // 判断如果集合类型是EnumSet类型,则调用clone方法拷贝
    if (c instanceof EnumSet) {
        return ((EnumSet<E>)c).clone();
    } else {
        // 如果集合是空,直接抛异常
        if (c.isEmpty())
            throw new IllegalArgumentException("Collection is empty");
        // 通过Iterator迭代器实现拷贝
        Iterator<E> i = c.iterator();
        E first = i.next();
        EnumSet<E> result = EnumSet.of(first);
        while (i.hasNext())
            result.add(i.next());
        return result;
    }
}

copyOf方法要求传入的集合参数不能是空集合,并且拷贝方式是通过迭代器的方式来实现的。其中还有一个重载的copyOf方法是调用该方法来实现的。

complementOf()

public static <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s) {
    EnumSet<E> result = copyOf(s);
    result.complement();
    return result;
}

这个方法不太好翻译,或许可以理解是一个过滤,或者说去除已有元素的方法。拿一个例子解释下:

public static void main(String[] args) {
    EnumSet enumSet = EnumSet.of(Flag.RIGHT, Flag.ERROR);
    System.out.println(enumSet);

    EnumSet set = EnumSet.complementOf(enumSet);
    System.out.println(set);
}

上述enumSet中有枚举Flag的两个元素,通过complementOf方法之后,会得到枚举Flag中除了这两个元素之外的剩余元素。

  • 该方法底层调用的是complement方法,EnumSet中没有对该方法进行实现,具体的实现是相应的实现类来实现的。

range()

public static <E extends Enum<E>> EnumSet<E> range(E from, E to) {
    if (from.compareTo(to) > 0)
        throw new IllegalArgumentException(from + " > " + to);
    EnumSet<E> result = noneOf(from.getDeclaringClass());
    result.addRange(from, to);
    return result;
}

前面已经提过,EnumSet是有序的,所以该方法是创建一个区间内的EnumSet,通过传入枚举类型的两个元素,获取该枚举类型里这两个元素之间的数据,同时也包含这两个元素。同样,底层的addRange方法也是抽象方法,由相应的实现类来实现。

总结

EnumMap和EnumSet对特定的Enum对象做了优化,在特定的的情况下,性能可以获得提升。

EnumMap是专门为枚举类型量身定做的Map实现。虽然使用其它的Map实现(如HashMap)也能完成枚举类型实例到值得映射,但是使用EnumMap会更加高效:它只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值。这使得EnumMap的效率非常高。EnumMap在内部使用枚举类型的ordinal()得到当前实例的声明次序,并使用这个次序维护枚举类型实例对应值在数组的位置。

1、父类为AbstractMap,未实现Map接口,只实现了Cloneable和Serializable接口。 2、非线程安全,所有方法和操作都未加锁。 3、采用key数组和vals数组共同实现key和value的关联。 4、不允许null key,但允许null value。 5、null值会被转换为Object的NULL实例占位替换。 6、元素的存储顺序按照枚举值的声明次序存储。

作者:程序那些事
链接:juejin.cn/post/684490…
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:Misout
链接:www.jianshu.com/p/d86f9d8af…
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:骑着乌龟去看海
链接:www.jianshu.com/p/f73d7066e…
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。