文章目录
枚举和注解
java支持两种特殊用途的引用类型:一种是类,称作枚举类型enum type;一种是接口,称作注解类型annotation type
34.用enum代替int常量
枚举类型是指由一组固定的常量组成合法值的类型,以前使用int枚举模式即private static final int APPLE = 0,缺点在于int枚举模式是编译时常量constant variable ,若常量关联的值发生改变必须重新编译
而枚举类型解决了以前枚举模式的缺点
public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
java的枚举本质上是int值
java枚举类型的基本想法:这些类通过公有的静态final域为每个枚举常量导出一个实例。枚举类型没有可以访问的构造器,所以它是真正的final类。客户端不能创建枚举类型的实例,也不能对它进行扩展,因此不存在实例,只存在声明过的枚举常量。即枚举类型实例受控,它们是单例的泛型化,本质上是单元素的枚举
优点:
- 保证了编译时的类型安全,即传参Apple,则只接受Apple枚举类的常量
- 允许多个枚举类型的同名常量在一个系统中和平共处
- 可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码
枚举类型还允许添加任意的方法和域,并实现任意的接口。它们提供了所有Object方法的高级实现,实现了Comparable和Serializable接口,并针对枚举类型的可任意改变性设计了序列化方式
为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器
枚举类型有一个静态的values方法,按照声明顺序返回它的值数组。toString方法返回每个枚举值的声明名称
当把一个元素从一个枚举类型中移除时,不会影响传入枚举参数的客户端代码,除非客户端直接引用了被移除的元素
将不同的行为与每个枚举常量关联:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体constant-specific class body中,用具体的方法覆盖每个常量的抽象apply方法,这种方法被称作特定于常量的方法实现constant-specific method implementation
public enum Operation {
PLUS("+"){
@Override
public double apply(double x, double y){return x + y;}
},
MINUS("-") {
@Override
public double apply(double x, double y){return x - y;}
},
TIMES("*") {
@Override
public double apply(double x, double y){return x * y;}
},
DIVIDE("/") {
@Override
public double apply(double x, double y){return x / y;}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
public abstract double apply(double x, double y);
@Override
public String toString() {
return symbol;
}
}
本质上PLUS,MINUS,TIMES,DIVIDE都是枚举类型,且是Operation的子类
枚举类型有一个自动产生的valueOf(String)方法,他将常量的名字转变成常量本身。若在枚举类型中覆盖toString方法,要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举。
要求每个常量都有一个独特的字符串表示法
private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect(Collectors.toMap(Object::toString, e -> e));
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
枚举常量创建之后,Operation常量从静态代码块中被放入了到了stringToEnum的映射中。除了编译时常量域外,枚举构造器不可以访问枚举的静态域。这个限制有一个特例:枚举常量无法通过其构造器访问另一个构造器。
枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为
public static Operation inverse(Operation op) {
switch(op) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new AssertionError("Unknown op:" + op);
}
}
如果有多个枚举常量同时共享相同的行为,则要考虑策略枚举:
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay() {
// default
this(PayType.WEEKDAY);
}
PayrollDay(PayType payType) {
this.payType = payType;
}
int pay(int minsWorked, int payRate) {
return payType.pay(minsWorked, payRate);
}
// the strategy enum type
private enum PayType {
WEEKDAY {
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
// 分钟计 每天8h
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
枚举类型通常在装载和初始化时需要空间和时间成本
每当需要一组固定常量,并且在编译时就知道其成员的时候,就应该使用枚举
枚举类型中的常量集并不一定要始终保持不变
35.用实例域代替序数
所有枚举都有一个ordinal方法,它返回每个枚举常量在类型中的数字位置。
切记,永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中
public enum Ensemble {
SOLE(1);
private final int num;
Ensemble(int size) {
this.num = size;
}
public int num() {
return num;
}
}
Enum规范中,ordinal是设计用于像EnumSet和EnumMap这种基于枚举的通用数据结构的
36. 用EnumSet代替位域
public class Test {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public void applyStyles(int styles) {...}
}
这种表示法让你用OR位运算将几个常量合并到一个集合中,称作位域bit field:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
EnumSet类能有效地表示从单个枚举类型中提取的多个值的多个集合。这个类实现Set接口,在内部具体的实现上,每个EnumSet内容都表示为位矢量。
如果底层的枚举类型有64个或者更少的元素,整个EnumSet就使用单个long来表示,因此它的性能比得上位域的性能。
使用枚举代替位域:
public class Text {
public enum Style {BOLD, ITALIC}
public void applyStyles(Set<Style> styles) {...}
}
以下将EnumSet实例传递给applyStyles方法的客户端代码。EnumSet提供了丰富的静态工厂来轻松创建集合:
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
EnumSet的缺点在于无法创建不可变的EnumSet
正是因为枚举类型要用在集合中,所以没有理由用位域来表示它
37.用EnumMap代替序数索引
有的类会利用ordinal方法来索引数组或列表
public class Plant {
enum LifeCycle {ANNUAL, PERENNIAL, BIENNIAL}
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
main方法:
Set<Plant>[] plantsByLifeCycle = new Set[LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) {
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
以上存在的问题:编译器无法知道序数和数组索引之间的关系
有一种更好的方法就是不用数组充当从枚举到值的映射,而是用EnumMap专门用于枚举键
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
注意:EnumMap构造器采用键类型的Class对象:这是一个有限制的类型令牌,他提供了运行时的泛型信息
基于stream的代码
Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle));
这段代码的问题在于它选择自己的映射实现,而不是EnumMap,为了解决这个问题要使用三种参数的Collectors.groupingBy方法,它允许调用者利用mapFactory参数定义映射实现:
Arrays.stream(graden).collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet()));
在进行大量使用映射的程序中需要以上优化
当遇到两次索引时可利用EnumMap,(起始阶段, Map(目标阶段, 阶段过渡))
最好不要用序数来索引数组,而要使用EnumMap
若表示的关系时多维的则使用EnumMap<…, EnumMap<…>>
38.用接口模拟可扩展的枚举
对于可伸缩的枚举类型而言,至少有一种具有说服力的用例,即操作码operation code,一般不对枚举进行扩展
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+"){
@Override
public double apply(double x, double y){return x + y;}
},
MINUS("-") {
@Override
public double apply(double x, double y){return x - y;}
},
TIMES("*") {
@Override
public double apply(double x, double y){return x * y;}
},
DIVIDE("/") {
@Override
public double apply(double x, double y){return x / y;}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
虽然BasicOperation不是可扩展的,但是接口类型Operation却是可扩展的,它用来表示API中的操作的接口类型,可以定义另一个枚举类型,实现这个接口,并用这个新类型的实例代替基本类型。
由求幂和求余操作组成
public enum ExtendedOperation implements Operation {
EXP("^") {
@Override
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
@Override
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
注意,在枚举中,不必像在不可扩展的枚举中所做的那样,利用特定于实例的方法实现(34)来声明抽象的apply方法。因为抽象的方法apply是接口的一部分
不仅可以在任何需要基本枚举的地方单独传递一个扩展枚举的实例,而且除了那些基本类型的元素之外,还可以传递完整的扩展枚举类型,并使用它的元素。
main方法
test(ExtendedOperation.class, x, y);
private static <T extends Enum<T> & Operation> void test(Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants()) {}
}
第二种方法是传入一个Collection<? extends Operation>,这是个有限制的通配符类型,而不是传递一个类对象
main:
test(Arrays.asList(ExtendedOperation.values(), x, y));
private static void test(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet) {}
}
它允许调用者将多个实现类型的操作合并到一起,但是放弃了在指定操作上使用EnumSet和EnumMap的功能
接口模拟可伸缩枚举的缺点:无法将实现从一个枚举类型继承到另一个枚举类型。需要将共享功能封装在一个辅助类或者静态辅助方法中
若实现代码不依赖任何状态,就可以将缺省实现放在接口中(20)
虽然无法编写可扩展的枚举类型,却可以编写接口以及实现该接口的基础枚举类型来对它进行模拟
39.注解优先于命名模式
命名模式的缺点:
- 文字拼写错误会导致失败,且没有提示
- 无法确保它们只用于相应的程序元素上
- 它们没有提供将参数值与程序元素关联起来的好方法
注解解决了上述问题
模拟Junit4的test注解
// 运行时有效
@Retention(RetentionPolicy.RUNTIME)
// 修饰方法
@Target(ElementType.METHOD)
public @interface Test {
}
其中@Retention,@Target是修饰注解的元注解meta-annotation
像@Test这样没有参数只是标注被注解的元素的叫标记注解marker annotation
注解不会改变被注解代码的语义,但是使它可以通过工具进行特殊处理
只能用于无参静态方法
private static void test(String className) throws Exception {
Class<?> testClass = Class.forName(className);
int tests = 0;
int passed = 0;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
// 此处执行公有静态方法
m.invoke(null);
passed++;
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
System.out.println(m + "failed:" + t);
} catch (Exception e) {
System.out.println("Invalid @Test: " +m);
}
}
}
System.out.println(String.format("Passed:%d, Failed: %d%n", passed, tests - passed));
}
有参数的注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
// 有限制的类型令牌 允许注解的用户指定任何异常或错误类型
Class<? extends Throwable> value();
}
实例:
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void t1() { // Test should pass
int i = 0;
i = i / i;
System.out.println("t1 instance");
}
@ExceptionTest(ArithmeticException.class)
public static void t2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
System.out.println("t2");
}
@ExceptionTest(ArithmeticException.class)
public static void t3() { // Should fail (no exception)
System.out.println("t3 ");
}
}
private static void test(String className) throws Exception {
Class<?> testClass = Class.forName(className);
int tests = 0;
int passed = 0;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.println(String.format("Test %s failed: no exception %n", m));
} catch (InvocationTargetException e) {
Throwable exc = e.getCause();
Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();
// obj instanceof Class
// Class.isInstance(obj) 效果等同上者 obj是否可以转化为这个Class
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.println(String.format("Test %s failed: expected %s, got %s%n", m, excType.getName(), exc));
}
} catch (Exception e) {
System.out.println("Invalid @Test: " +m);
}
}
}
}
一般进行多值注解需要定义一个数组参数即,Class<? extends Exception>[] value();
而从java8开始,不用数组参数声明一个注解类型而是用@Repeatable元注解对注解的声明进行注解,表示该注解可以被重复地应用给单个元素。
@Repeatable只有一个参数,就是包含注解类型containing annotation type的类对象,他唯一的参数是一个注解类型数组
注意:包含的注解类型必须利用适当的保留策略和目标进行注解,否则无法编译
@Repeatable(ExceptionTestContainer.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
重复注解代替数组值注解之后的代码:
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doubleBad() {}
此时获取注解通过Annotation的方法getAnnotationsByType(Class)获取数组而不是通过getAnnotation方法;且利用isAnnotationPresent检测重复和非重复地注解,必须检查注解类型及其包含的注解类型:如下
if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.println(String.format("Test %s failed: no exception %n", m));
} catch (InvocationTargetException e) {
Throwable exc = e.getCause();
ExceptionTest[] excTypeTests = m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest excTypeTest : excTypeTests) {
Class<? extends Throwable> excType = excTypeTest.value();
// obj instanceof Class
// Class.isInstance(obj) 效果等同上者 obj是否可以转化为这个Class
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.println(String.format("Test %s failed: expected %s, got %s%n", m, excType.getName(), exc));
}
}
} catch (Exception e) {
System.out.println("Invalid @Test: " +m);
}
}
40.坚持使用Override注解
在你想要覆盖超类声明的每个方法声明中使用Override注解
由于缺省方法的出现,在接口方法的具体实现上使用Override可以确保签名正确;若接口没有缺省方法,可以选择省略接口方法的具体实现上的Override注解
41.用标记接口定义类型
标记接口marker interface是不包含方法声明的接口,它只是指明一个类实现了具有某种属性的接口。如Serializable接口,通过实现这个接口,类表明它的实例可以被写到ObjectOutputStream中(被序列化)
标记接口有两点胜过标记注解。
- 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。
标记接口类型的存在,允许你在编译时就能捕捉到在使用标记注解的情况下要到运行时才能捕捉到的错误
比如Serializable,序列化时只能传入实现这个接口的实例,否则编译报错,但遗憾的是序列化传参是Object,没有使用上述优点
若使用注解方式,只有运行时尝试序列化一个不可序列化的对象才会报错
- 标记接口胜过标记注解的另一个优点是,它们可以被更加精确地进行锁定
注解类型用目标ElementType.TYPE声明,它就可以被应用于任何类或者接口。而一个标记只适用于特殊接口的实现,此时将它定义为标记接口则确保所有被标记的接口都是该唯一接口的子类型
如Set接口是这种有限制的标记接口restricted marker interface 它只适用于Collection子类型
标记注解胜过标记接口的最大优点在于,它们是更大的注解机制的一部分
因此标记注解在那些支持注解作为编程元素之一的框架中同样具有一致性
若标记是应用于任何程序元素而不是类或者接口则使用注解否则优先使用标记接口(标记是广泛使用注解框架的一个组成部分则选择标记注解)
若你发现自己在编写的目标为ElementType.TYPE的标记注解类型,就要花时间考虑清楚,它是否真的应该为注解类型,想想标记接口是否会更加合适