用EnumMap代替序数索引
用EnumMap代替序数索引
有时候,会见到利用ordinal方法来索引数组的代码。例如下面这个简化的类,表示一种烹饪用的香草:
public class Herb {
public enum Type { ANNUAL, PERENNIAL, BIENNIAL }
private final String name;
private final Type type;
Herb(String name, Type type) {
this.name = name;
this.type = type;
}
@Override
public String toString() {
return name;
}
}
假设有一个香草的数组,表示一座花园中的植物,想要按照类型(一年生、多年生或者两年生植物)进行组织后将植物列出来。
有些程序员会将这些集合放到一个按照类型序号进行索引的数组实现这一点。
// Using ordinal() to index an array - DON'T DO THIS
public static void main(String[] args) {
Herb[] garden = {new Herb("a", Herb.Type.ANNUAL), new Herb("b", Herb.Type.BIENNIAL)};
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: [a]
PERENNIAL: []
BIENNIAL: [b]
这种方法可行,但是隐藏许多问题。但是由于数组与泛型不兼容,需要进行未受检的转换,并且不能正确无误的编译。因为数组不知道它的索引代表着什么,你必须手工标注这些索引的输出。但是这种方法最严重的问题在于,当你访问了一个按照枚举的序数进行索引的数组时,使用正确的int值就是你的责任了;int不能够提供枚举的类型安全。如果使用了错误的值,程序就会悄悄地完成错误的工作,或者幸运的话,会抛出ArraysIndexOutOfBoundException异常。
幸运的是,有一种更好的方法可以达到同样的效果。数组实际上充当着从枚举到值的映射,因此可能还要用到Map。更具体的说,有一种非常快速的Map实现专门用于枚举键,称作Java.util.EnumMap。
public static void main(String[] args) {
Herb[] garden = {new Herb("a", Herb.Type.ANNUAL), new Herb("b", Herb.Type.BIENNIAL)};
// Using an EnumMap to associate data with an enum
Map<Herb.Type, Set<Herb>> herbByType = new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);
for(Herb.Type t : Herb.Type.values())
herbByType.put(t, new HashSet<Herb>());
for(Herb h : garden)
herbByType.get(h.type).add(h);
System.out.println(herbByType);
}
{ANNUAL=[a], PERENNIAL=[], BIENNIAL=[b]}
这段代码更简短、更清楚,也更安全,运行速度方面可以与使用序数的程序相媲美。它没有不安全的转换;不必手工标注这些索引的输出,因为映射键应该知道如何将自身翻译成可打印的字符串的枚举;计算数组索引时也不可能出错。EnumMap在运行速度方面之所以能与通过序数索引的数组相媲美,是因为EnumMap在内部使用了这种数组。但是它对程序员隐藏了这种实现细节,集Map的丰富功能和类型安全与数组的快速于一身。注意EnumMap构造器采用键类型的Class对象:这是一个有限的类型令牌,它提供了运行时的泛型信息。
你还可能见到按照序数进行索引(两次)的数组的数组,该序数表示两个枚举值的映射。例如下面的程序就是使用这样的一个数组将两个阶段映射到一个阶段过渡中(从液体到固体称作凝固,从液体到气体称作沸腾,诸如此类)。
//Using ordinal() to index array of arrays -DON'T DO IS
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOTL, CONDENSE, SUBLIME, DEPOSIT;
private static final Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME},
{FREEZE, null, BOTL},
{DEPOSIT, CONDENSE, null}
};
public static Transition from(Phase src, Phase dst) {
return TRANSITIONS[src.ordinal()][dst.ordinal()];
}
}
}
这段代码可行,看起来也很优雅,但是事实并非如此。就像上面那个比较简单的香草花圆的示例一样,编译器无法知道序数和索引之间的关系。如果在过渡表中出了错,或者在修改Phase或者Phase.Transition枚举类型的时候忘记了将它更新,程序就会在运行时失败。这种失败的形式可能是ArrayIndexOutOfBoundsException、NullPointerException或者没有任何错误的提示的错误行为。这张表的大小是阶段个数的平方,即使非null项的数量比较少。
同样,利用EnumMap依然可以做的更好一些。因为每个阶段的过度都是通过一对阶段枚举进行索引的,最好将这种关系表示为一个map,这个map的键是一个枚举,值为另一个map(起始阶段),这第二个map的键为第二个枚举(目标阶段),它的值为结果(阶段过渡),即形成了Map(起始阶段,Map(目标阶段,阶段过渡))这种形式。一个阶段过渡所关联的两个阶段,最好通过“数据与阶段过渡枚举之间的关联”来获取,之后用该阶段过渡枚举来初始化嵌套的EnumMap。
//Using a nested EnumMap to associate data with enum pairs
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID,LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase src;
private final Phase dst;
Transition(Phase src, Phase dst) {
this.src = src;
this.dst = dst;
}
private static final Map<Phase, Map<Phase, Transition>> m =
new EnumMap<Phase, Map<Phase, Transition>>(Phase.class);
static {
for(Phase p : Phase.values())
m.put(p, new EnumMap<Phase, Transition>(Phase.class));
for(Transition t : Transition.values())
m.get(t.src).put(t.dst, t);
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
}
初始阶段过渡的map的代码看起来可能有点复杂,但是还不算太糟糕。map的类型为Map<Phase,Map<Phase,Transition>>
,表示是由键为源Phase(即第一个phase)、值为另外一个map组成的Map,其中组成值的Map是由键值对目标Phase(即第二个Phase)、Transition组成的。静态初始化代码块中的第二个循环初始化了外部map,得到了三个空的内容map。代码块中的第二个循环利用每个状态过渡常量提供的起始信息和目标信息初始化了内部map。代码块中的第二个循环利用每个状态过度常量提供的起始信息和目标信息初始化了内部的map。
现阶段新增加一个新的阶段:plasma(离子)或者电离气体。只有两个过渡与这个阶段相关联:电离化,它将气体变成离子;以及消电离化,将离子变成气体。为了更新基于数组的程序,必须添加Phase添加一种新的常量,给Phase.Transition添加两种常量,用新的16个元素的版本取代原来9个元素的数组的数组。如果给数组添加过多或者过少,或者元素放置不妥当,可就麻烦了:程序可编译,但是会运行失败。为了更新EnumMap的版本,所要做的就是必须将PLASMA添加到Phase列表,并将IONSIE(GAS,PLASMA)和DEIONIZE(PLASMA,GAS)添加到Phase.Transition的列表中。程序会自行处理其他的事情,你几乎没有机会出错。从内部来看,Map的Map被实现成了数组的数组,因此提升了清楚行、安全性和易维护性的同时,在空间上或者时间上还几乎不用任何开销。
总而言之,最好不要用序数来索引数组,而要使用EnumMap。如果你所表示的这种关系是多维的,就使用EnumMap<...,EnumMap<...>>
。应用程序的程序员在一般情况下都不使用Enum.ordinal,即使要用也很少,因此这是一种特殊情况。
EnumMap实现
/**
* All of the values comprising K. (Cached for performance.)
*/
private transient K[] keyUniverse;
用EnumMap代替序数索引的更多相关文章
- Effective Java 第三版——37. 使用EnumMap替代序数索引
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- 【Effective Java】9、使用EnumMap代替序数索引
package cn.xf.cp.ch02.item33; import java.util.EnumMap; import java.util.HashSet; import java.util.M ...
- 第33条:用EnumMap代替序数索引
有时候,会见到利用ordinal方法来索引数组的代码.例如下面这个简化的类,表示一种烹饪用的香草: public class Herb { public enum Type { ANNUAL, PER ...
- 使用 ENUMMAP 替代序数索引
import java.util.Arrays; import java.util.EnumMap; import java.util.HashSet; import java.util.Map; i ...
- effective java 学习心得
目的 记录一下最主要学习心得,不然凭我这种辣鸡记忆力分分钟就忘记白看了... 用静态工厂方法代替构造器的最主要好处 1.不必每次都创建新的对象 Boolean.valueOf Long.valueOf ...
- Effective java笔记(五),枚举和注解
30.用enum代替int常量 枚举类型是指由一组固定的常量组成合法值的类型.在java没有引入枚举类型前,表示枚举类型的常用方法是声明一组不同的int常量,每个类型成员一个常量,这种方法称作int枚 ...
- Effective Java 读书笔记之五 枚举和注解
Java1.5中引入了两个新的应用类型家族,新的类为枚举类型,新的接口为注解类型. 一.用enum代替int常量 1.枚举值由一组固定的常量组成合法值的类型. 二.用实例域代替序数 1.不要根据枚举的 ...
- Effective Java 阅读笔记——枚举和注解
30:用enum代替int常量 当需要一组固定常量的时候,应该使用enum代替int常量,除了对于手机登资源有限的设备应该酌情考虑enum的性能弱势之外. 31:用实例域代替序数 应该给enum添加i ...
- [Effective Java]第六章 枚举和注解
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
随机推荐
- SURF matlab 检测函数使用
1.这篇介绍SURF检测blob的函数. 函数/Functions 函数名称:detectSURFFeatures 功能:利用The Speeded-Up Robust Features(SURF)算 ...
- Machine Learning in Action(5) SVM算法
做机器学习的一定对支持向量机(support vector machine-SVM)颇为熟悉,因为在深度学习出现之前,SVM一直霸占着机器学习老大哥的位子.他的理论很优美,各种变种改进版本也很多,比如 ...
- 03-树3 Tree Traversals Again(25 point(s)) 【Tree】
03-树3 Tree Traversals Again(25 point(s)) An inorder binary tree traversal can be implemented in a no ...
- jmeter之java请求
通常情况下,推荐使用jmeter之java请求编写一beashell调用java代码(上篇)(推荐)编写Java 请求 有以下优势 脚本易维护 易调试 开发脚本周期短 不过网上扩展java请求文章比较 ...
- java基础以及操作Excle
今天把会经常用的几个集合的迭代方法又练习了一下,放在这里,经常复习! map集合迭代 /*** 迭代map[1]*/ for (Integer key : map.keySet()) {//迭代key ...
- MySQL学习笔记(五)—— 子查询及联结
子查询: 子查询,即嵌套在其他查询中的查询.例如我们有这样几个表,顾客表,订单表,商品表,我们想知道有哪些客户买了商品A,那么我们就需要先查看哪些订单里包含了商品A,然后根据订单查出是哪些客户. my ...
- 让Outlook一直保持开启
1.将OutLook.exe注册为服务,让其一直保持开启状态 类似于TaobaoProtect.exe是由TBSecSvc服务启动的 http://stackoverflow.com/question ...
- system调用命令行命令而不显示命令行窗口
system调用命令行命令而不显示命令行窗口 通常用system调用命令行命令时都会弹出黑底白字的命令行窗口,下面的代码可以不显示弹出的命令行窗口. 代码如下 #pragma comment( lin ...
- chan_ss7 呼出的时候指定使用某个CICs,或者CICs范围 的方法
Linkset one: 1-31 Incoming 33-58 Outgoing 58-63 Emergency Traffic Linkset two: 1-31 Incoming 33-58 ...
- bzoj 2169 连边——去重的思想
题目:https://www.lydsy.com/JudgeOnline/problem.php?id=2169 如果之前都去好重了,可以看作这次连的边只会和上一次连的边重复. 可以认为从上上次的状态 ...