关键词搜索

源码搜索 ×
×

Java集合框架:EnumMap

发布2016-03-17浏览14779次

详情内容


欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。


欢迎跳转到本文的原文链接:https://honeypps.com/java/java-collection-enummap/

##EnumMap定义

package java.util;

import java.util.Map.Entry;
import sun.misc.SharedSecrets;
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;
}

      keyType变量是EnumMap的key泛型的类对象,EnumMap根据这个类型,可以获得keyUniverse的内容,vals存放的是与keyUniverse映射的值,如果没有映射则为null,如果映射为null则会特殊处理成NULL,NULL的定义如下:

     private static final Object NULL = new Object() {
            public int hashCode() {
                return 0;
            }
    
            public String toString() {
                return "java.util.EnumMap.NULL";
            }
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

      对于值NULL的处理类似WeakHashMap的特殊处理,会有两个方法:

        private Object maskNull(Object value) {
            return (value == null ? NULL : value);
        }
    
        private V unmaskNull(Object value) {
            return (V) (value == NULL ? null : value);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      这样可以区分vals中是null(即没有映射)还是NULL(即映射为null);
      EnumMap的size是根据vals中的非null(包括NULL)的值的个数确定的,比如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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

      typeCheck判断key的类对象或者父类对象是否与keyType相等,如果不相等则抛出ClassCastException异常。
      注意EnumMap并没有类似HashMap的resize的过程,也没有加载因子的概念,因为在一个EnumMap创建的时候,keyUniverse和vals的大小就固定。


    ##EnumMap使用
      先举个小例子:

    package collections.map;
    
    import java.util.EnumMap;
    import java.util.Map;
    
    public class EnumMapTest
    {
        public enum Color
        {
            RED,BLUE,BLACK,YELLOW,GREEN;
        }
        public static void main(String[] args)
        {
            EnumMap<Color,String> map = new EnumMap<>(Color.class);
            EnumMap<Color,String> map = new EnumMap<>(Color.class);
            map.put(Color.YELLOW, "黄色");
            map.put(Color.RED, "红色");
            map.put(Color.BLUE, null);
    //        map.put(null, "无");   //会报NullPonitException的错误
            map.put(Color.BLACK, "黑色");
            map.put(Color.GREEN, "绿色");
    
            for(Map.Entry<Color,String> entry:map.entrySet())
            {
                System.out.println(entry.getKey()+":"+entry.getValue());
            }
            System.out.println(map);
        }
    }
    
      12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

      运行结果:

    RED:红色
    BLUE:null
    BLACK:黑色
    YELLOW:黄色
    GREEN:绿色
    {RED=红色, BLUE=null, BLACK=黑色, YELLOW=黄色, GREEN=绿色}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

      EnumMap的key不允许为null,value可以为null,按照key在enum中的顺序进行保存,非线程安全。可以用工具类Collections进行包装成线程安全的:

    Map<EnumKey, V> m = Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));
    
    • 1

      有关enum的应用知识可以参考《Java枚举类型enum》。
      EnumMap的基本操作都比较快,都在常量时间内完成,基本上(但不保证)比HashMap快。
      EnumMap有三个构造函数:

    • public EnumMap(Class<K> keyType);
    • public EnumMap(EnumMap<K, ? extends V> m);
    • public EnumMap(Map<K, ? extends V> m) ;

      前两个构造函数一目了然,对第三个构造函数进行分析:

    Map<Integer,Integer> map1 = new HashMap<>();
            map1.put(1, 1);
            map1.put(3, 3);
            map1.put(2, 2);
            Map<Integer,Integer> map2 = new EnumMap<>(map1);//编译器提示错误:Cannot infer type arguments for EnumMap<>
    
    • 1
    • 2
    • 3
    • 4
    • 5

      这个是因为Integer并不是extends Enum;
      这里变换一下,采用Map<Enum,Integer> map1 = new HashMap<>();试试看呢:

    Map<Enum,Integer> map1 = new HashMap<>();
            map1.put(Color.YELLOW, 1);
            map1.put(Color.RED, 3);
            map1.put(Color.BLUE, 2);
            Map<Enum,Integer> map2 = new EnumMap<>(map1);
    
            for(Map.Entry entry:map2.entrySet())
            {
                System.out.println(entry.getKey()+":"+entry.getValue());
            }
            System.out.println(map2);
            System.out.println(map2.size());
    
      12

      能够正常运行,输出结果:

    RED:3
    BLUE:2
    YELLOW:1
    {RED=3, BLUE=2, YELLOW=1}
    3
    
    • 1
    • 2
    • 3
    • 4
    • 5

      相信大家能够总结个一二了吧。


    EnumMap用途

      《Effective Java》中作者建议用EnumMap代替叙述索引,最好不要用序数来索引数组,而要使用EnumMap
      这里采用《Effective Java》书中的例子来举例。

         public static class Herb
        {
            public enum Type
            {
                ANNUAL, PERENNIAL, BIENNTAL
            }
    
            private final String name;
            private final Type type;
    
            public Herb(String name, Type type)
            {
                this.name = name;
                this.type = type;
            }
    
            public Type getType()
            {
                return type;
            }
    
            @Override
            public String toString()
            {
                return name;
            }
        }
    
      12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

      现在用一座种满香草的花园,想要按照类型(一年生、多年生、两年生,即上面Type的类型)进行组织之后将这些植物列出来。如果使用数组实现的话,需要构建三个集合,每种类型一个,并且遍历整座花园,将每种香草放到相应的集合中。

    Herb[] garden = new Herb[]{new Herb("f1",Herb.Type.ANNUAL),new Herb("f2",Herb.Type.PERENNIAL),new Herb("f3",Herb.Type.BIENNTAL),
                    new Herb("f4",Herb.Type.PERENNIAL),new Herb("f5",Herb.Type.ANNUAL),new Herb("f6",Herb.Type.BIENNTAL),
                    new Herb("f7",Herb.Type.ANNUAL),new Herb("f8",Herb.Type.BIENNTAL),new Herb("f9",Herb.Type.PERENNIAL)};
    
            Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length];
            for(int i=0;i<herbsByType.length;i++)
            {
                herbsByType[i] = new HashSet<Herb>();
            }
            for(Herb h:garden)
            {
                herbsByType[h.type.ordinal()].add(h);
            }
            for(int i=0;i<herbsByType.length;i++)
            {
                System.out.printf("%s:%s%n", Herb.Type.values()[i],herbsByType[i]);
            }
    
      12
    • 13
    • 14
    • 15
    • 16
    • 17

      运行结果:

    ANNUAL:[f5, f7, f1]
    PERENNIAL:[f4, f2, f9]
    BIENNTAL:[f8, f3, f6]
    
    • 1
    • 2
    • 3

      这种方法确实可行,但是影藏着许多问题。因为数组不能和泛型兼容,程序需要进行未受检的转换,并且不能正确无误地进行编译。因为数组不知道它的索引代表着什么,你必须手工标注这些索引的输出。但是这种方法最严重的问题在于,当你访问一个按照枚举的叙述进行索引的数组时,使用正确的int值就是你的职责了,int不能提供枚举的类型安全。
      但是你可以用EnumMap改善这个程序:

    Herb[] garden = new Herb[]{new Herb("f1",Herb.Type.ANNUAL),new Herb("f2",Herb.Type.PERENNIAL),new Herb("f3",Herb.Type.BIENNTAL),
                    new Herb("f4",Herb.Type.PERENNIAL),new Herb("f5",Herb.Type.ANNUAL),new Herb("f6",Herb.Type.BIENNTAL),
                    new Herb("f7",Herb.Type.ANNUAL),new Herb("f8",Herb.Type.BIENNTAL),new Herb("f9",Herb.Type.PERENNIAL)};
            Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<>(Herb.Type.class);
            for(Herb.Type t : Herb.Type.values())
            {
                herbsByType.put(t, new HashSet<Herb>());
            }
            for(Herb h:garden)
            {
                herbsByType.get(h.type).add(h);
            }
            System.out.println(herbsByType);
    
      12
    • 13

      运行结果:

    {ANNUAL=[f7, f1, f5], PERENNIAL=[f4, f2, f9], BIENNTAL=[f8, f6, f3]}
    
    • 1

      这段程序更剪短、更清楚,也更安全,运行速度方面可以与使用序数的数组相媲美。注意EnumMap构造器采用键类型的Class对象:这是一个有限制的类型令牌,它提供了运行时的泛型信息


    总结

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


    参考资料:

    1. Java枚举类型enum
    2. 《Effective Java(Second Edition)》. Joshua Bloch.
    3. EnumMap与Enumset的使用

    欢迎跳转到本文的原文链接:https://honeypps.com/java/java-collection-enummap/

    欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。


    相关技术文章

    点击QQ咨询
    开通会员
    返回顶部
    ×
    微信扫码支付
    微信扫码支付
    确定支付下载
    请使用微信描二维码支付
    ×

    提示信息

    ×

    选择支付方式

    • 微信支付
    • 支付宝付款
    确定支付下载