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


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

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

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

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

List<Clothes> 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<K, ? extends V> m)
public EnumMap(Map<K, ? extends V> m)

比如:

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

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

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

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

这个方法可以这么使用:

List<Clothes> 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<stat.length; i++){
System.out.println(Size.values()[i]+": "+ stat[i]);
}

输出为:

SMALL 3
MEDIUM 1
LARGE 2

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

实现原理

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

内部组成

EnumMap有如下实例变量:

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

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

构造方法

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

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

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

private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> 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编程及计算机技术的本质。用心原创,保留所有版权。

计算机程序的思维逻辑 (50) - 剖析EnumMap的更多相关文章

  1. 计算机程序的思维逻辑 (29) - 剖析String

    上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比 ...

  2. 计算机程序的思维逻辑 (51) - 剖析EnumSet

    上节介绍了EnumMap,本节介绍同样针对枚举类型的Set接口的实现类EnumSet.与EnumMap类似,之所以会有一个专门的针对枚举类型的实现类,主要是因为它可以非常高效的实现Set接口. 之前介 ...

  3. 计算机程序的思维逻辑 (30) - 剖析StringBuilder

    上节介绍了String,提到如果字符串修改操作比较频繁,应该采用StringBuilder和StringBuffer类,这两个类的方法基本是完全一样的,它们的实现代码也几乎一样,唯一的不同就在于,St ...

  4. 计算机程序的思维逻辑 (31) - 剖析Arrays

    数组是存储多个同类型元素的基本数据结构,数组中的元素在内存连续存放,可以通过数组下标直接定位任意元素,相比我们在后续章节介绍的其他容器,效率非常高. 数组操作是计算机程序中的常见基本操作,Java中有 ...

  5. 计算机程序的思维逻辑 (48) - 剖析ArrayDeque

    前面我们介绍了队列Queue的两个实现类LinkedList和PriorityQueue,LinkedList还实现了双端队列接口Deque,Java容器类中还有一个双端队列的实现类ArrayDequ ...

  6. 计算机程序的思维逻辑 (53) - 剖析Collections - 算法

    之前几节介绍了各种具体容器类和抽象容器类,上节我们提到,Java中有一个类Collections,提供了很多针对容器接口的通用功能,这些功能都是以静态方法的方式提供的. 都有哪些功能呢?大概可以分为两 ...

  7. 计算机程序的思维逻辑 (38) - 剖析ArrayList

    从本节开始,我们探讨Java中的容器类,所谓容器,顾名思义就是容纳其他数据的,计算机课程中有一门课叫数据结构,可以粗略对应于Java中的容器类,我们不会介绍所有数据结构的内容,但会介绍Java中的主要 ...

  8. 计算机程序的思维逻辑 (40) - 剖析HashMap

    前面两节介绍了ArrayList和LinkedList,它们的一个共同特点是,查找元素的效率都比较低,都需要逐个进行比较,本节介绍HashMap,它的查找效率则要高的多,HashMap是什么?怎么用? ...

  9. 计算机程序的思维逻辑 (46) - 剖析PriorityQueue

    上节介绍了堆的基本概念和算法,本节我们来探讨堆在Java中的具体实现类 - PriorityQueue. 我们先从基本概念谈起,然后介绍其用法,接着分析实现代码,最后总结分析其特点. 基本概念 顾名思 ...

随机推荐

  1. C# 利用性能计数器监控网络状态

    本例是利用C#中的性能计数器(PerformanceCounter)监控网络的状态.并能够直观的展现出来 涉及到的知识点: PerformanceCounter,表示 Windows NT 性能计数器 ...

  2. 关于VS2015 ASP.NET MVC添加控制器的时候报错

    调试环境:VS2015 数据库Mysql  WIN10 在调试过程中出现类似下两图的同学们,注意啦. 其实也是在学习的过程中遇到这个问题的,找了很多资料都没有正面的解决添加控制器的时候报错的问题,还是 ...

  3. [.NET] 怎样使用 async & await 一步步将同步代码转换为异步编程

    怎样使用 async & await 一步步将同步代码转换为异步编程 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/6079707.html  ...

  4. zookeeper源码分析之二客户端启动

    ZooKeeper Client Library提供了丰富直观的API供用户程序使用,下面是一些常用的API: create(path, data, flags): 创建一个ZNode, path是其 ...

  5. iOS 多线程之GCD的使用

    在iOS开发中,遇到耗时操作,我们经常用到多线程技术.Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法,只需定义想要执行的任务,然后添加到适当的调度队列 ...

  6. 易用BPM时代,企业如何轻松驾驭H3?

    众所周知,BPM作为企业发展的推动力,能敏捷高效的融合业务流程和信息资源.通过综合考虑流程的成本.效率.质量等方面因素,用IT系统将调整后的流程固化下来,从而降低企业管理成本,提高内部运营效率,提升企 ...

  7. git和pycharm管理代码

    首先明白三个概念,服务器代码库,本地代码库,和正在coding的项目. coding完毕后,先通过commit提交到本地代码库,然后通过push再提交server的代码库    git步骤 git c ...

  8. 使用gulp解决RequireJS项目前端缓存问题(二)

    1.前言 这一节,我们主要解决在上一节<使用gulp解决RequireJSs项目前端缓存问题(一)>末尾提到的几个问题: 对通过require-config.js引入的js文件修改后,没有 ...

  9. 换个角度看微信小程序[推荐]

    去年参加几次技术沙龙时,我注意到一个有意思的现象:与之前大家统一接受的换名片不同,有些人并不愿意被添加微信好友--"不好意思,不熟的人不加微信". 这个现象之所以有意思,是因为名片 ...

  10. SQL Server2014 SP2关键特性

    SQL Server2014 SP2关键特性 转载自:https://blogs.msdn.microsoft.com/sqlreleaseservices/sql-2014-service-pack ...