Java集合框架: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";
        }
    };

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

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

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

  这样可以区分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);
    }

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

  运行结果:

RED:红色
BLUE:null
BLACK:黑色
YELLOW:黄色
GREEN:绿色
{RED=红色, BLUE=null, BLACK=黑色, YELLOW=黄色, GREEN=绿色}

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

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

  有关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<>

  这个是因为Integer并不是extends Enum;
  这里变换一下,采用Map

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

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

RED:3
BLUE:2
YELLOW:1
{RED=3, BLUE=2, YELLOW=1}
3

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


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

  现在用一座种满香草的花园,想要按照类型(一年生、多年生、两年生,即上面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]);
        }

  运行结果:

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

  这种方法确实可行,但是影藏着许多问题。因为数组不能和泛型兼容,程序需要进行未受检的转换,并且不能正确无误地进行编译。因为数组不知道它的索引代表着什么,你必须手工标注这些索引的输出。但是这种方法最严重的问题在于,当你访问一个按照枚举的叙述进行索引的数组时,使用正确的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);

  运行结果:

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

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


总结

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



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

时间: 2024-08-05 04:29:03

Java集合框架:EnumMap的相关文章

Java集合框架:总结

最近博主对于Java集合框架这个系列做了一个整理,主要包括: Map系:HashMap, LinkedHashMap, TreeMap, WeakHashMap, EnumMap; List系:ArrayList, LinkedList, Vector, Stack; Set系:HashSet, LinkedHashSet, TreeSet; 工具类:Collections,Arrays 不过并没有对多线程(ConcurrentHashMap,BlockingQueue等)集合框架进行整理,以后

Java集合源码剖析:Java集合框架

Java集合工具包位于Java.util包下,包含了很多常用的数据结构,如数组.链表.栈.队列.集合.哈希表等.学习Java集合框架下大致可以分为如下五个部分:List列表.Set集合.Map映射.迭代器(Iterator.Enumeration).工具类(Arrays.Collections). Java集合类的整体框架如下: 从上图中可以看出,集合类主要分为两大类:Collection和Map. Collection是List.Set等集合高度抽象出来的接口,它包含了这些集合的基本操作,它主

JAVA集合框架之List接口实现类

上一篇博客<JAVA集合框架之Set接口实现类>中介绍了Set接口的相关实现类,这一篇将介绍List接口的实现类. java.util.ArrayList< E > ArrayList有点类似于数组,相比较于数组而言,ArrayList可以动态的更改元素个数,相对于数组较为灵活. 每个 ArrayList 实例都有一个容量.该容量是指用来存储列表元素的数组的大小.它总是至少等于列表的大小.随着向 ArrayList 中不断添加元素,其容量也自动增长.并未指定增长策略的细节,因为这不

java集合框架中List的定义及注意事项

大家知道,集合框架是为了表示和操作集合而规定的一种统一的标准的体系结构,学习集合知识有利于我们解决一系列例如保存数据与对象的问题. 常用的集合在系统中定义了两大接口,List和Set 这里我们就来讨论一下List 的定义以及一些常见的问题 List定义的是有序的并且数据可以重复的集合,我们先看一下下面这段代码: import java.util.ArrayList; import java.util.List; publicclass ListTest{ publicstaticvoid mai

java | 集合框架

集合框架 集合代表了一组对象,Java中的集合框架定义了一套规范,用来表示.操作集合,使具体操作与实现细节解耦. 而这些操作无非就是增.删.改.查! 集合和数组的区别: 1.数组的长度固定,集合长度可变. 2.数组只能存储相同类型的数据(基本类型/引用类型),集合可存储各种类型的数据. Java集合框架接口 Java集合框架的顶层接口包括: 一.Collection接口: 1.实现Collection接口的集合有List.Set.Queue(Java队列实现). 2.List:排列有序,可以有重

[Java] 集合框架的层次结构和使用规则梳理

在Java语言中,Java语言的设计者对常用的数据结构和算法做了一些规范(接口)和实现(具体实现接口的类).所有抽象出来的数据结构和操作(算法)统称为Java集合框架(JavaCollectionFramework). Java程序员在具体应用时,不必考虑数据结构和算法实现细节,只需要用这些类创建出来一些对象,然后直接应用就可以了,这样就大大提高了编程效率. 概述 什么是框架?  类库的集合 什么是集合? 存放数据的容器 集合框架用来干什么? 用来表示和操作的统一的架构 集合框架包含了两部分:一

java基础-学到java集合框架中对那个复写equals的疑问,求解答

问题描述 学到java集合框架中对那个复写equals的疑问,求解答 import java.util.*; class Student implements Comparable { private String name; private int age; Student(String name,int age) { this.name = name; this.age = age; } public int compareTo(Student s) { int num = new Inte

Java集合框架学习总结

<Java集合框架学习总结> 以下介绍经常使用的集合类,这里不介绍集合类的使用方法,只介绍每个集合类的用途和特点,然后通过比较相关集合类的不同特点来让我们更深入的了解它们.   Collection接口 1.Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements). 2.所有实现Collection接口的类都必须提供两个标准的构造函数:1.无参数的构造函数用于创建一个空的Collection,2.Collection

数据结构-java集合框架里说的堆栈和内存存储里说的堆栈两个堆栈是一个概念么

问题描述 java集合框架里说的堆栈和内存存储里说的堆栈两个堆栈是一个概念么 最近在看java集合框架,有列表啊,散列,堆栈等概念.感觉这是以数组,java代码为基础的处理数据集合的容器.但是之前经常 看到说内存就是种堆和栈,而内存这块是jvm帮我们处理的,也就是说不是java代码关心的内容.而且还有篇博客说了内存的控制 是由汇编实现的,然后将一段C++代码编程成汇编,汇编指令实际是按堆栈模式工作的. 本人有点疑惑,感觉数据结构里的堆栈和内存中的堆栈的实现不是一回事啊,想问下两者是不同的层面上的

JAVA集合框架之Set接口实现类

在上一篇<JAVA集合框架>中为大家介绍了JAVA集合框架的基本组成,这一片开始将为大家介绍集合框架中常用的实现类的用法. java.util.HashSet< E > 此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持.它不保证 set 的迭代顺序:特别是它不保证该顺序恒久不变.此类允许使用 null 元素.这是我们最常用的Set接口的实现类. 构造方法 方法名 说明 HashSet() 构造一个新的空 set,其底层 HashMap 实例的默认初始容量是