枚举类型添加以前使用常量的方式来满足使用需要,但是这种方式有很多缺点:
无法保证类型安全,并且没有没有表现力(不够优雅)。
常量是跟随调用者编译的,如果常量改变了但是调用者没有重新编译那么会出现问题
不利于debug,因为你打印出来的都是一些常量。
枚举保证了一个编译时类型安全,避免出现枚举滥用错用的情况;同时枚举是单例,并且无法扩展,这也保证了枚举的安全性。
同时为了获取常量值,需要定义一系列方法和构造器来供外部访问,同时为了方便debug可以重写 toString
方法。
如果有需要,可以让枚举私有、包内私有化或者作为内部类,自然如果需要枚举的共用或者重用可以把他放在顶级包内。
避免使用 ordinal
方法,因为枚举的修改都会导致不可预期的序号变换;
大部分程序员都不需要这样的一个功能,除非是一个特殊的数据结构比如 Enumset
和 EnumMap
需要遍历枚举的时候。
比如下面的代码:
// Bit field enumeration constants - OBSOLETE! public class Text { public static final int STYLE_BOLD = 1 << 0; // 1 public static final int STYLE_ITALIC = 1 << 1; // 2 public static final int STYLE_UNDERLINE = 1 << 2; // 4 public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8 // Parameter is bitwise OR of zero or more STYLE_ constants public void applyStyles(int styles) { ... } } text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
这种写法无法直观的修改和检查值,并且也很难去遍历常量,
同时你并不能很简单的去检查数据是否溢出,
并且一旦确定常量就很难再去作修改,除非修改你的API。
这个时候使用枚举集合就可以很好的解决这个问题:
// EnumSet - a modern replacement for bit fields public class Text { public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } // Any Set could be passed in, but EnumSet is clearly best public void applyStyles(Set<Style> styles) { ... } } text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
很直观并且清晰。
注意上面的方法参数是 Set<Style>
而不是 EnumSet
,这种时候方法参数最好接收一个接口类而不是他的实现,
这样就可以给接口的各种实现提供尽可能多的支持(item 64)
总的来说就是需要在集合中使用枚举的时候没有理由用位属性来替代 EnumSet
,
EnumSet
唯一缺点就是无法在java9之前创建一个不变的 EnumSet
,
但可以封装 Collections.unmodifiableSet
方法到 EnumSet
中,不过这样会破坏 EnumSet
的简洁和性能。
用书中一个很典型的例子来进一步说明:
// Using ordinal() to index array of arrays - DON'T DO THIS! public enum Phase { SOLID, LIQUID, GAS; public enum Transition { MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT; // Rows indexed by from-ordinal, cols by to-ordinal private static final Transition[][] TRANSITIONS = { { null, MELT, SUBLIME }, { FREEZE, null, BOIL }, { DEPOSIT, CONDENSE, null } }; // Returns the phase transition from one phase to another public static Transition from(Phase from, Phase to) { return TRANSITIONS[from.ordinal()][to.ordinal()]; } } }
用 Phase
来表示物体的三种状态(固态,液态,气态),同时 Transition
来表示每种状态间转换过程,定义一个二维数组来表示,
方法 from
返回任意两种状态之间的转换过程。
代码看着是很美好,也很简短。
首先在二维数组中有null容易导致空指针,如果再加一个状态PLASMA(等离子态),
代码修改就比较麻烦,特别是二维数组的维护(顺序错误的话就会返回错误的结果)。
使用 EnumMap
来实现:
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 from; private final Phase to; Transition(Phase from, Phase to) { this.from = from; this.to = to; } // Initialize the phase transition map private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values()).collect(groupingBy(t -> t.from, () -> new EnumMap<>(Phase.class), toMap(t -> t.to, t -> t, (x, y) -> y, () -> new EnumMap<>(Phase.class)))); public static Transition from(Phase from, Phase to) { return m.get(from).get(to); } } }
Transition
维护的是枚举(转换过程的枚举),列出了所有的转换状态,
内部属性 m
则初始化存储了一个 Map<Phase, EnumMap<Phase, Transition>>
,
初始化方法调用的是java8的流方法来对 m
作一个初始化,
Phase
作为key,value存储的则是该状态转换为其他状态的所有过程。
from
方法就只需要get返回即可。
这样写虽然代码量增加了,但是可维护性提升了,如果需要增加一个PLASMA,
只需要在 PHASE
中增加枚举,同时 Transition
中增加可能的转换过程即可。
上面的代码基本不可能给你犯错的机会,除非你连固态到气态的过程都能搞错。
总的来说不要使用枚举的 ordinal
去作为数组索引,而应该使用 EnumMap
。
这一节主要讲的是枚举的一个扩展性,由于枚举是不能被继承的,导致扩展性很差
如果有扩展的需要考虑将扩展点提取到一个接口中,枚举实现该接口,其他的枚举按照需要实现该接口即可。
命名模式的缺点(让我想起了JPA):
Java8中引入了可重复注解 Repeatable
:
重复注解只使用在注解字段中存在数组的情况,同时 Repeatable
注解必须指定可以重复的注解,该注解只包含一个数组字段;
getAnnotationsByType
方法可以同时获取重复和非重复注解;
使用 isAnnotationPresent
方法判断是否有某个注解时需要格外注意:
如果参数传入实际写在代码中的注解,则会忽略只包含一个数组字段的注解(真正意义上重复的注解),
如果参数传入上面后者,则会忽略上面前者;
所以使用该方法时需要判断两个注解是否存在。
重复注解的出现旨在提升代码可读性,看需求使用。
如果代码需要调用者传入参数到源码中就使用注解而不是去使用命名模式。
虽然大部分的编码都不需要定义注解,但是java内置的一些注解还是应该使用的(比如 Override
),
这些注解已经成立了标准使用之前还是需要多看看。
如果不使用 Override
注解,比如 equals
方法,就会出现一些意外情况,使用注解同时会帮助你检查是否正确的覆盖了父类的方法。
大部分IDE都会在你实现接口或者覆盖父类方法的同时自动加上注解。
当然如果覆盖了一个抽象方法,这个时候你就没必要添加 Override
注解了。
标记接口定义:没有声明任何方法的接口,比如Java api中的 Serializable
,表示某个实现该接口的对象可以被序列化。
标记注解定义类似,两者都只是作一个标记,表明某个功能或用途。
标记接口相对于标记注解的好处:
ElementType.TYPE
),它可以被使用到任何可以使用的地方, Set
接口则是一个限制的标记接口,它只适用于 Collection
的子类。 而标记注解相对标记接口的优点则是:可在一个注解集成的框架中使用。
那么如何正确的使用它们?
如果标记是作用于元素(属性)的话肯定是使用注解,
而作用于类或者是接口,并且会写一些只接收标记对象方法的话则使用标记接口,这样就会让你传入接口作为参数,同时在编译期间做了类型检查。
当你定义的注解目标是 ElementType.TYPE 的时候,则需要思考使用哪个才是更为合适。
这一章介绍的两个主要内容:
枚举,需要谨慎使用 ordinal
方法,多考虑使用 EnumSet
EnumMap
;
注解,简单介绍使用方式;
BugHome版权所有丨转载请注明出处:https://minei.me/archives/486.html