您的位置:首页 > Web前端

EffectiveJava第六章:枚举和注解

2017-03-21 22:08 148 查看
讨论枚举和注解的最佳实践。

30. 用enum代替int常量

枚举类型(enum type)是指由一组固定的常量组成合法值得类型,在编程语言还没有引入枚举之前,表示枚举类型的常用模式是声明一组具名的int常量,称作int枚举模式

int枚举模式的不足

它在类型安全性和使用方便性方法没有任何帮助

采用int枚举模式的程序是十分脆弱的。因为int枚举是编译时常量,被编译到使用它们的客户端中,如果int值发生变化,则客户端就必须重新编译。如果不编译,则它们的行为就是不确定的。

打印出int枚举常量的字符串,并没有很便利的方法。要便利一个组中所有的int枚举常量、获取int枚举组大小等这些都没有很可靠的方法。

String枚举模式

int枚举模式的变体,同样也有很大的不足。虽然它提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作;

枚举enum,Java的枚举本质上是int

它们就是通过公有的静态final域为每个枚举常量导出实例的类,因为没有可访问的构造器,枚举类型是真正的final;不能实例化,只能访问已声明过的枚举常量。

即枚举类型是实例受控的,它们是单例的泛型化,本质上式单元素的枚举。

包含同名常量的多个枚举类型可以在同一个系统中和平共处,因为每个类型都有自己的命名空间(在不同的类下)。

可以增加或重排枚举类型中的常量,而无需重新编译它的客户端代码,因为导出的常量域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中,而是在int枚举模式之中。

可以调用toString()方法将枚举转换成可打印的字符串。

可以添加任意的方法和域,并可以实现任意的接口。

可以遍历

与枚举常量关联的有些行为,可能只需要用在定义了枚举的类或者包中。这种行为最好被实现成私有的或者包级私有的方法。

如果一个枚举具有普遍适用性,就应该成为一个顶层类;如果它只是用在一个特定的顶层类中,它应该被作为该顶层类的一个成员类。

例如:java.math.RoundingMode枚举类。

有时候需要在不同的枚举类型上表现本质不同的行为,采用如下这种方法:在枚举类型中声明一个抽象方法,并在特定常量的类主体(constant-specific class body)中实现该抽象方法。

这种方法被称作特定于常量的方法实现(constant-specific method implementation)

public enum Operation {
PLUS {
@Override double apply(double x, double y) {
return x + y;
}
},
MINUS {
@Override double apply(double x, double y) {
return x - y;
}
},
TIMES {
@Override double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
@Override double apply(double x, double y) {
return x / y;
}
};

abstract double apply(double x, double y);
}


特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得困难。

可以增加抽象方法、调用不同的辅助方法,但这样会产生大量的样板代码,降低可读性。

可以使用策略枚举(strategy enum),即嵌套一个私有枚举,把大量计算放在其中,外部将调用该嵌套枚举常量来委托计算。虽然这种模式没有switch语句那么简洁,但更加安全、灵活。

枚举的性能缺点:

装载和初始化枚举时会有空间和时间的成本。

总之,与int常量相比,枚举类型有很大优势,要易读、更安全、功能更强大。善用枚举,另外如果多个枚举常量同时共享相同的行为时,考虑使用策略枚举。

31. 用实例域代替序数

可以使用枚举的
ordinal
方法获取到该枚举常量在枚举中的排列位置,但依赖该方法的话维护就会很麻烦;比如添加一个枚举或重排,就会导致该位置发生变化。

所以永远不要根据枚举的序数导出与它关联的值,而是使用枚举值的成员变量来保存,通过构造方法传入。

最好完全避免使用
ordinal
方法,它是设计成用在EnumSet、EnumMap这种基于枚举的通用数据结构的。

32. 用EnumSet代替位域

/*
使用int常量进行位运算,位域
*/
public class Text {
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 2;
public static final int STYLE_UNDERLINE = 1 << 3;

public void applyStyles(int styles) {
}

//text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
}

/*
使用EnumSet代替位域
*/
public class TextEnumSetStyle {
public enum Style {
BOLD, ITALIC, UNDERLINE;
}

public void applyStyles(Set<Style> styles) { //使用接口接收,说不定传其他的Set实现
}

//text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));
}


位域有着int枚举常量的所有缺点,不能打印、不能遍历等

EnumSet实现了Set接口,提供了丰富的功能和类型安全性,以及可以从其他任何Set实现中得到的互用性。

在内部实现上,每个EnumSet内容都表示为矢量;如果枚举类型<=64个,整个EnumSet就是用单个long来表示,所以性能比得上位域。

EnumSet类集位域的简洁和性能优势、及枚举类型的所有优点于一身,但枚举本身是消耗性能的,使用时考虑考虑。

33. 使用EnumMap代替序数索引

EnumMap的运行速度方面可以与使用序数的程序相媲美,它没有不安全的转换;不必通过索引输出。

EnumMap在内部使用了数组, 索引运行速度和通过序数索引的数组一样。

EnumMap构造器采用的键类型是Class对象:有限制的类型令牌(bounded type token),它提供了运行时的泛型信息。

EnumMap使用Enum作为键,值可为任意。当你需要使用Enum时,并且需要通过该枚举类型作为序数索引时,就考虑使用EnumMap。

34. 用接口模拟可伸缩的枚举

让枚举实现接口来扩展枚举。

用接口来扩展枚举的缺点是:枚举无法继承,所以代码不能复用;少量的可以复制粘贴,但如果有大量的共享代码,则可以封装在一个辅助类或静态辅助方法中。

虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。这样运行客户端编写自己的枚举来实现接口。

35. 注解优先于命名模式

举例:JUnit测试框架原本要求它的用户一定要用test作为测试方法开头,后来改成使用注解标明。

命名模式的缺点:

文字拼写错误会导致失败,且没有任何提示

无法确保它们只用于相应的程序元素上,比如该用在方法上却用在类上。

它们没有提供将参数值和程序元素关联起来的好方法。

注解很好的解决所有这些问题。

注解中数组参数的语法十分灵活,它是进行过优化的单元素数组。

//@Target(ElementType.Method)
//@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String[] value() default "";
}


使用时
@MyAnnotation({"1","2"})
来声明参数。

在编写框架时,比如JUnit、ButterKnife之类的注解框架,可以有效的使用注解来解耦、提高性能,应该避免使用命名模式。

36. 坚持使用Override注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}


@Override
注解只能用在方法声明上,表示被注解的方法覆盖了父类中的一个方法。

应该在你想要覆盖超类方法的每个方法声明中使用Override注解。可以避免把覆盖误用成重载。这是利用了注解编译期进行检查的功能。IDE也会提示你进行了错误的覆盖。

在抽象类或者接口中,应当标注所有你想要的方法,来覆盖超类或者接口的方法,无论是具体的还是抽象的。

37. 用标记接口定义类型

标记接口(marker interface)是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口。

标记接口胜于标记注解

标记接口定义的类型是由被标记类的实例实现的,标记注解则没有定义这样的类型;这个类型可以捕捉到编译期间的错误,而标注注解可能要在运行时才能发现。

标记接口可以被更精确的锁定。

例如Set接口是Collection接口的子类型,虽然它不是标记接口,但可以说成是
有限制的标记接口
。这种标记接口可以描述整个对象的某个约束条件(例如Set),或者表明实例能够利用其它每个类的方法进行处理(例如ObjectOutputStream需要Serializable接口进行标记)。

标记注解胜于标记接口

标记注解可以通过默认的方式添加一个或多个注解类型元素,给已被使用的注解类型添加更多的信息。即简单的标记注解类型可以演变成更加丰富的注解类型。这对于标记接口则是不可能的,因为标记接口不能在实现它了之后再随意添加方法

标记注解的另一个优点在于它是更大的注解机制的一部分。因此标记注解在那些大量使用注解的框架中具有一致性。

什么时候用标记接口、标记注解呢,在写代码时可以多考虑考虑

如果标记是应用到任何元素时,使用标记注解;

如果只用于类和接口,就要考虑这个被标记的类的实例所用的地方多不多?

如果多,使用标记接口就可以传入该接口类型,做到编译期进行检查。

如果不多,就再考虑是否永远限制这个标记只用于特殊类、接口么?如果是永远限制,就做成一个接口的子接口进行限制;如果不是永远限制,就可以使用标记注解。

标记接口和标记注解都各有用处:

如果想要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择;

如果该标记在未来可能要给标记添加更多的信息,或者标记要适合于已经广泛使用了注解类型的框架,那么就选择标记注解。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: