博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java编程的逻辑 (50) - 剖析EnumMap
阅读量:6492 次
发布时间:2019-06-24

本文共 6014 字,大约阅读时间需要 20 分钟。

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接


我们提到,如果需要一个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
countBySize(List
clothes){ Map
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
map = new EnumMap<>(Size.class);

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

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

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

List
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
m)public EnumMap(Map
m)

比如:

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

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

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

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

这个方法可以这么使用:

List
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

输出为:

SMALL 3MEDIUM 1LARGE 2

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

实现原理

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

内部组成

EnumMap有如下实例变量:

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

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

构造方法

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

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

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

private static 
> K[] getKeyUniverse(Class
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;}

遍历值数组进行比较。

根据键删除

remove方法的代码为:

public V remove(Object key) {    if (!isValidKey(key))        return null;    int index = ((Enum)key).ordinal();    Object oldValue = vals[index];    vals[index] = null;    if (oldValue != null)        size--;    return unmaskNull(oldValue);}

代码也很简单,就不解释了。

实现原理小结

以上就是EnumMap的基本实现原理,内部有两个数组,长度相同,一个表示所有可能的键,一个表示对应的值,值为null表示没有该键值对,键都有一个对应的索引,根据索引可直接访问和操作其键和值,效率很高。

小结

本节介绍了EnumMap的用法和实现原理,用法上,如果需要一个Map且键是枚举类型,则应该用它,简洁、方便、安全,实现原理上,内部使用数组,根据键的枚举索引直接操作,效率很高。

下一节,我们来看枚举类型的Set接口的实现类EnumSet,与之前介绍的Set的实现类不同,它内部没有用对应的Map类EnumMap,而是使用了一种极为高效的方式,什么方式呢?

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

转载地址:http://cghuo.baihongyu.com/

你可能感兴趣的文章
leetcode 15. 3Sum
查看>>
数据结构排序问题---堆排序及各种排序时间空间复杂度
查看>>
POJ-2739(Water)
查看>>
【转】第三节 UNIX文件系统结构
查看>>
为什么sql里面not in后面的子查询如果有记录为NULL的,主查询就查不到记录
查看>>
-1-0 Java 简介 java是什么 java简单介绍
查看>>
认识uWSGI、uwsgi、wsgi
查看>>
获取手机号
查看>>
BZOJ1263 [SCOI2006]整数划分
查看>>
虚函数表中除了正常函数指针,还有其他的指针
查看>>
EasyUI中datagrid显示异常问题——行宽不起作用
查看>>
SSH隧道
查看>>
蓝桥杯 第七届 JAVA B组 凑算式
查看>>
BPDU与PortFast
查看>>
关于windows2008r2系统80端口被system进程占用的问题
查看>>
【转】WebResource实现在自定义控件中内嵌JS文件
查看>>
java中的CountDownLatch
查看>>
第一篇:GCD多线程的概念
查看>>
Angular7里面实现 debounce search
查看>>
Linux 内核链表
查看>>