声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将追究法律责任!原文链接:http://www.cnblogs.com/jiangzhengjun/p/4255663.html

第六章      枚举和注解

30、      用enum代替int常量

枚举类型是指由一组固定的常量组成合法值的类型,例如一年中的季节或一副牌中的花色。在没引入枚举时,一般是声明一组int常量,每个类型成员一个常量:

public static final int APPLE_FUJI = 0;

public static final int APPLE_PIPPIN = 1;

public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;

public static final int ORANGE_TEMPLE = 1;

public static final int ORANGE_BLOOD = 2;

这种方法称作int枚举模式,存在很多不足,不具有类型安全与使用方便性。如果你将apple传到一个想要接收orange的方法中,编译器也不会出现警告,而且还可以使用==来比较apple与orange。

注意每个apple常量都以APPLE_作为前缀,每个orange常量都以ORANGE_作为前缀,这是因为可以防止名称冲突。

采用int枚举模式的程序是十分脆弱,因为int枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的int发生了变化,客户端就必须重新编译,如果不重新编译,程序还是可以运行,但不是最新的值了。

另外从使用方便性来看,没有便利的toString方法,打印出来的为数字,没有多大的用处。要遍历一组中所有的int枚举常量,也没有可靠的方法。

既然int枚举常量有这么多的缺点,那使用String枚举常如何?同样也不是我们期望的。虽然在可以打印字符串,但它会导致性能问题,因为它依赖于字符串的比较操作。另外与int枚举常量一样会编译到客户端代码中,编译时难以发现,但会在运行时出错。

幸运的是1.5版本开始,枚举可以避免int和String枚举模式的缺点,并提供许多额外的好处。下面是最简单的形式:

public enum Apple{FUJI,PIPPIN,GRANNY_SMITH}

public enum Orange{NAVEL,TEMPLE,BLOOD}

Java枚举类型背后的基本想法很简单:本质上是int值,它们是通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。因为客户端即不能创建枚举类型的实例,也不能对它进行扩展,因此对它进行实例化,而只有声明过的枚举常量。换句话说,枚举类型是实例受控的。它们是单例的泛型化,本质上是单元素的枚举。

枚举提供了编译时类型安全。如果声明一个参数的类型为Apple,就可以保证,被传到该参数上的任何非null的对象引用一定属于三个有效的Apple值之一。试图传递类型错误的值时,会导致编译时错误,就像试图将某种枚举类型的表达式赋值给另一种枚举类型的变量,或者试图利用==操作符比较不同枚举类型的值一样,都会出错。

枚举提供了单独的命名空间,同一系统中可以有多个同名的枚举类型变量。你可以增加或者重新排序枚举类型常量,而无需重新编译它的客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中,而是在int枚举模式之中。最终,可以通过调用toString方法,将枚举转换成可打印的字符串。

除了完善了int枚举模式不足外,枚举还允许添加任意的方法和域,并实例任意接口,它们提供了所有Object(见第3章)的高级实现,实现了Comparable和Serializable接口,并针对枚举型的可任意改变性设计了序列化方式。

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

可以为枚举类型添加数据域与方法,下面是一个算术运行的枚举类:

public enum Operation {

PLUS("+") {

double apply(double x, double y) {

return x + y;

}

},

MINUS("-") {

double apply(double x, double y) {

return x - y;

}

},

TIMES("*") {

double apply(double x, double y) {

return x * y;

}

},

DIVIDE("/") {

double apply(double x, double y) {

return x / y;

}

};

private final String symbol;//操作符:+ - * /

Operation(String symbol) {//构造函数,存储操作符供toString打印使用

this.symbol = symbol;

}

@Override

//重写Enum中的打印name的性为

public String toString() {

return symbol;

}

//抽像方法,不同的常量具有不同的功能,需在每个常量类的主体里重写它

abstract double apply(double x, double y);

/*

*  初始化时,存储操作符与枚举常量的对应关系,用来实现 fromString 方法

*  这样我们就可以通过 操作符来获取到对应的枚举常量,有点像valueOf方法,

*  只不过它是通过枚举常量的名字name来获取常量的。这种通用的方法还可以

*  应用到其他枚举类中

*/

private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();

static { // 从name到枚举常量转换到从某个域到枚举常量的转换

for (Operation op : values())

stringToEnum.put(op.toString(), op);

}

// 根据操作符来获取对应的枚举常量,如果没有返回null,模拟valueOf方法

public static Operation fromString(String symbol) {

return stringToEnum.get(symbol);

}

public static void main(String[] args) {

double x = Double.parseDouble(args[0]);

double y = Double.parseDouble(args[1]);

for (Operation op : Operation.values())

System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));

for (Operation op : Operation.values())

System.out.printf("%f %s %f = %f%n", x, op, y, Operation

.fromString(op.toString()).apply(x, y));

}

}

在opr包下会看见Operation.class、Operation$4.class、Operation$2.class、Operation$3.class 、Operation$1.class这样几个类,Operation$X.class都是继承自Operation类,而Operation又继承自Enum类,下面是反编译这些类的代码:

public abstract class opr.Operation extends java.lang.Enum{

public static final opr.Operation PLUS;

public static final opr.Operation MINUS;

public static final opr.Operation TIMES;

public static final opr.Operation DIVIDE;

private final java.lang.String symbol;

private static final java.util.Map stringToEnum;

private static final opr.Operation[] ENUM$VALUES;

static {};

private opr.Operation(java.lang.String, int, java.lang.String);

public java.lang.String toString();

abstract double apply(double, double);

public static opr.Operation fromString(java.lang.String);

public static void main(java.lang.String[]);

public static opr.Operation[] values();

public static opr.Operation valueOf(java.lang.String);

opr.Operation(java.lang.String, int, java.lang.String, opr.Operation);

}

class opr.Operation$1 extends opr.Operation{

opr.Operation$1(java.lang.String, int, java.lang.String);

double apply(double, double);

}

枚举构造器不可以访问枚举的静态域,除了编译时常量域之外,这一限制是有必要的,因为构造器运行的时候,这些静态域还没有被初始化。

枚举常量中的方法有一个美中不足的地方,它们使用在枚举常量中共享代码变得更加因难了。例如,考虑用一个枚举来实现星期中的工资数。算法是这样的,在五个工作日中,除正常的工作时间外,算加班;在双休日中,所有工作时数都算加班时间,下面是第一次简单的实现:

public enum PayrollDay {

MONDAYTUESDAYWEDNESDAYTHURSDAYFRIDAYSATURDAYSUNDAY;

private static final int HOURS_PER_SHIFT = 8;//正常工作时数

/**

* 工资计算

@param hoursWorked 工作时间(小时)

@param payRate 每小时工资

@return

*/

double pay(double hoursWorked, double payRate) {

//基本工资,注这里使用的是double,真实应用中请不要使用

double basePay = hoursWorked * payRate;

double overtimePay;//加班工资,为正常工资的1.5倍

switch (this) {

case SATURDAY:

case SUNDAY://双休日加班工资

overtimePay = hoursWorked * payRate / 2;

default: //正常工作日加班工资

overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0

: (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;

break;

}

return basePay + overtimePay;//基本工资+加班工资

}

}

不可否认,这段代码很简单,但是从维护来看,非常危险。假设将一个元素添加到枚举中,如一个假期的特殊值,但忘了给switch语句添加相应的case,这时会计算出错。

为了针对不同的常量有不同的安全计算工资法,你必须重复每个常量的加班工资,或者将计算移到两个辅助方法中(一个用来计算工作日,一个用来计算双休日),并从每个常量调用相应的辅助方法。这任何一种方法都会产生很多的重复的样板代码,第二次如下实现:

public enum PayrollDay {

MONDAY() {

@Override

double overtimePay(double hoursWorked, double payRate) {

return weekdayPay(hoursWorked, payRate);

}

},

TUESDAY {

@Override

double overtimePay(double hoursWorked, double payRate) {

return weekdayPay(hoursWorked, payRate);

}

},

WEDNESDAY {

@Override

double overtimePay(double hoursWorked, double payRate) {

return weekdayPay(hoursWorked, payRate);

}

},

THURSDAY {

@Override

double overtimePay(double hoursWorked, double payRate) {

return weekdayPay(hoursWorked, payRate);

}

},

FRIDAY {

@Override

double overtimePay(double hoursWorked, double payRate) {

return weekdayPay(hoursWorked, payRate);

}

},

SATURDAY {

@Override

double overtimePay(double hoursWorked, double payRate) {

return weekendPay(hoursWorked, payRate);

}

},

SUNDAY {

@Override

double overtimePay(double hoursWorked, double payRate) {

return weekendPay(hoursWorked, payRate);

}

};

private static final int HOURS_PER_SHIFT = 8;//正常工作时数

//抽象出加班工资计算

abstract double overtimePay(double hoursWorked, double payRate);

//计算工资

double pay(double hoursWorked, double payRate) {

double basePay = hoursWorked * payRate;//公用

return basePay + overtimePay(hoursWorked, payRate);

}

//双休日加班工资算法

double weekendPay(double hoursWorked, double payRate) {

return hoursWorked * payRate / 2;

}

//正常工作日加班工资

double weekdayPay(double hoursWorked, double payRate) {

return hoursWorked <= HOURS_PER_SHIFT ? 0

: (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;

}

}

上面设计中存在很多的样板代码,如正常工作日都是调用weekdayPay方法来完成的,而双休都是调用weekendPay来完成的,有没有一种可以减少这些重复样板代码呢?请看下面:

enum PayrollDay {

MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(

PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(

PayType.WEEKEND), SUNDAY(PayType.WEEKEND);

private final PayType payType;//策略枚举类

PayrollDay(PayType payType) {

this.payType = payType;

}

double pay(double hoursWorked, double payRate) {

//计算委托给策略类

return payType.pay(hoursWorked, payRate);

}

// 嵌套的枚举策略类

private enum PayType {

WEEKDAY {//工作日枚举策略实例常量

double overtimePay(double hours, double payRate) {

return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)

* payRate / 2;

}

},

WEEKEND {//双休日枚举策略实例常量

double overtimePay(double hours, double payRate) {

return hours * payRate / 2;

}

};

private static final int HOURS_PER_SHIFT = 8;

abstract double overtimePay(double hrs, double payRate);

double pay(double hoursWorked, double payRate) {

double basePay = hoursWorked * payRate;

return basePay + overtimePay(hoursWorked, payRate);

}

}

}

虽然这种模式没有前面两种那么简单,便更加安全,也更加灵活。

从上面加班工资计算三种实现来看,如果多个枚举常量同时共享相同的行为时,则考虑策略枚举。

枚举适用于一组固定常量,当然枚举类型中的常量集并不一定要始终保持不变。

31、      不要使用ordinal,用实例域代替序数

永远不要根据枚举序数ordinal()导出与它关联的值,即不要依赖于枚举序数,否则重新排序这些枚举或添加新的常量,维护起来将是很困难的:

public enum Ensemble {

SOLODUETTRIOQUARTETQUINTETSEXTETSEPTET;

public int numberOfMusicians() {

return ordinal() + 1;

}

}

我们要将它保存在一个实例域中:

public enum Ensemble {

SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7);

private final int numberOfMusicians;

Ensemble(int size) {

this.numberOfMusicians = size;

}

public int numberOfMusicians() {

return numberOfMusicians;

}

}

Enum规范中谈到ordinal时这么定道:“大多数程序员都不需要这个方法。它是设计成用于像EunmSet和EnumMap这种基于枚举的通用数据结构”,除非你在编写的是这种数据结构,否则最好完全避免使用ordinal方法。

32、      用EnumSet代替位域

如果一个枚举类型的元素主要用在集合(组合)中,一般就使用int枚举模式,做法是将1向左移位来实现,这样就会有很多的组合形式,下面是四种字体样式的应用,可以组合出 2^4 – 1 = 15种样式来:

class Text{

public static final int STYLE_BOLD = 1 << 0;//1 字体加粗

public static final int STYLE_ITALTC = 1 << 1;// 2 斜体

public static final int STYLE_UNDERLINE = 1 << 2;//4 下划线

public static final int STYLE_STRIKETHROUGH = 1 << 3;//8 删除线

//应用样式

public void applyStyles(int styles){

//...

}

public static void main(String[] args) {

//应用粗体与斜体组合样式

new Text().applyStyles(STYLE_BOLD|STYLE_ITALTC);

}

}

位域表示法允许利用位操作,有效地执行了像组合和交集这样的集合操作。但位域有着int枚举常量的所有缺点,甚至更多,如当位域以数字形式打印时,翻译位域比翻译简单的(单个的)枚举常要困难得多。那么有没有一种好的方案来代替上面的设计呢?使用EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合。EnumSet内容都表示为位矢量,如果底层的枚举类型有64个或者更少的元素——大多如此——整个EnumSet就是用单个long来表示,因此它的性能比得上位域的性能。批处理,如removeAll和retainAll,都是利用位算法来实现的,就像手工替位域实现那样,但可以避免手工位操作时容易出现的错误以及复杂的代码。

下面是前一个实例改用枚举代替位域后的代码,它更加简短、清楚、安全:

public class Text {

public enum Style {

BOLDITALICUNDERLINESTRIKETHROUGH

}

/*

*  这里使用的Set接口而不是EnumSet类,最好还是使用接口

*  类型而非实现类型,这样还可以传递一些其他的Set实现

*/

public void applyStyles(Set<Style> styles) {

}

// Sample use

public static void main(String[] args) {

Text text = new Text();

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

}

}

总之,正是因为枚举类型可用在集合EnumSet中,所以没有理由用位域来表示它。EnumSet类集位域的简洁和性能优势及第30条中所述的枚举类型的所有优点于一身,用EnumSet代替位域就是理所当然的了。

33、      用EnumMap代替序数索引

EnumMap:与枚举类型键一起使用的专用 Map 实现。枚举映射中所有键都必须来自单个枚举类型,该枚举类型在创建映射时显式或隐式地指定。枚举映射在内部表示为数组。此表示形式非常紧凑且高效。

先来看一个能植物分类的实例,分类的标准是按照某枚举类型来分的:

public class Herb {

// 植物各类:一年生、多年生、两年生

static public enum Type {

ANNUALPERENNIALBIENNIAL

}

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;

}

public static void main(String[] args) {

//现有这样一些植物集合

Herb[] garden = { new Herb("Basil", Type.ANNUAL),

new Herb("Carroway", Type.BIENNIAL),

new Herb("Dill", Type.ANNUAL),

new Herb("Lavendar", Type.PERENNIAL),

new Herb("Parsley", Type.BIENNIAL),

new Herb("Rosemary", Type.PERENNIAL) };

//数组的索引与枚举Type对应 //问题一:需要进行未受检的转换

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: [Basil, Dill]

PERENNIAL: [Rosemary, Lavendar]

BIENNIAL: [Carroway, Parsley]

使用EnumMap对上面进行改进:

Herb[] garden =...

// 使用EnumMap并按照植物种类(枚举类型)来分类

Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(

Herb.Type.class);

for (Herb.Type t : Herb.Type.values())//初始化

herbsByType.put(t, new HashSet<Herb>());

for (Herb h : garden)//进行分类

herbsByType.get(h.type).add(h);

System.out.println(herbsByType);//输出

EnumMap在内部使用数组实现,在性能上与数组相当。但是它对程序员隐藏了实现细节,集Map的丰富功能和类型安全与数组的快速于一身。

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

枚举类型是不能被扩展的(继承),但使用接口可以解决这一问题,解决办法是让枚举类实现同一接口,在应用的地方以接口类型来传递参数,但这样会失去Enum类的某些特性:

// 枚举接口

public interface Operation {

double apply(double x, double y);

}

// 基础运算

public enum BasicOperation implements Operation {

PLUS("+") {

public double apply(double x, double y) {

return x + y;

}

},

MINUS("-") {

public double apply(double x, double y) {

return x - y;

}

},

TIMES("*") {

public double apply(double x, double y) {

return x * y;

}

},

DIVIDE("/") {

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;

}

}

//扩展运算

public enum ExtendedOperation implements Operation {

EXP("^") {

public double apply(double x, double y) {

return Math.pow(x, y);

}

},

REMAINDER("%") {

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;

}

}

测试:

private static <T extends Enum<T> & Operation> void test(Class<T> opSet,

double x, double y) {

for (Operation op : opSet.getEnumConstants())//失去Enum特性,使用反射

System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));

}

public static void main(String[] args) {

double x = Double.parseDouble(args[0]);

double y = Double.parseDouble(args[1]);

test(BasicOperation.class, x, y);

test(ExtendedOperation.class, x, y);

}

输出:

4.000000 + 2.000000 = 6.000000

4.000000 - 2.000000 = 2.000000

4.000000 * 2.000000 = 8.000000

4.000000 / 2.000000 = 2.000000

4.000000 ^ 2.000000 = 16.000000

4.000000 % 2.000000 = 0.000000

总之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。这样,客户端就能够编写自己的枚举来实现接口,如果API是根据接口编写的,那么在可以使用基础枚举类的地方,也都可以敷衍这些枚举。

35、      注解优先于命名模式

命名模式,表示有些程序元素需要通过某种工具或者框架进行特殊处理。例如,Junit框架原本要求它的用户一定要使用test作为测试方法名称的开头。

下面是一个简单的测试框架,使用注解来实现:

//专用于普通测试注解,该注解只适用于静态的无参方法,

//如果使用地方不正确由注解工具自己把握

@Retention(RetentionPolicy.RUNTIME)//注解信息保留到运行时,这样工具可以使用

@Target(ElementType.METHOD)//只适用于方法

public @interface Test {}

//专用于异常测试的注解

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface ExceptionTest {

//测试方法可能抛出多个异常

Class<? extends Exception>[] value();

}

下面应用上面定义的注解:

public class Sample {

@Test

public static void m1() {} // 测试应该通过

public static void m2() {}

@Test

public static void m3() { // 测试应该失败

throw new RuntimeException("Boom");

}

public static void m4() {}

@Test//不应该使用在这里,但应该由注解工具自己处理这种不当的使用

public void m5() {} // 错误使用: 非静态方法

public static void m6() {}

@Test

public static void m7() { // 测试应该失败

throw new RuntimeException("Crash");

}

public static void m8() {}

}

public class Sample2 {

@ExceptionTest(ArithmeticException.class)

public static void m1() { // 测试应该要通过,因为抛出了算术异常

int i = 0;

i = i / i;

}

@ExceptionTest(ArithmeticException.class)

public static void m2() { // 测试应该不通过,因为抛出的异常为数组越界异常

int[] a = new int[0];

int i = a[1];

System.out.println(i);

}

@ExceptionTest(ArithmeticException.class)

public static void m3() {

} // 测试应该不通过,因为没有抛也异常

// 可能抛出多个异常,使用{}括起来,如果是单个可以省略

@ExceptionTest( { IndexOutOfBoundsException.class,

NullPointerException.class })

public static void doublyBad() {

List<String> list = new ArrayList<String>();

//这里会抛出空指针错误,测试应该会通过

list.addAll(5, null);

}

}

T注解对应用类的语义没有直接的影响。注解永远不会改变被注解代码的语义,但是使用它可以通过工具进行特殊的处理,如下面注解工具实现类:

public class RunTests {

public static void main(String[] args) throws Exception {

int tests = 0;//需要测试的数量

int passed = 0;//测试通过的数量

//加载被注解的类,即被测试的类

Class<?> testClass = Class.forName(args[0]);

//遍历测试类的所有方法

for (Method m : testClass.getDeclaredMethods()) {

//Test注解实现工具

if (m.isAnnotationPresent(Test.class)) {

tests++;

try {

m.invoke(null);//没有参数,表示调用的是静态方法

passed++;//如果方法调用成功则表示测试通过

}//表示测试方法本身抛出了异常

catch (InvocationTargetException wrappedExc) {

Throwable exc = wrappedExc.getCause();

//打印测试方法抛出的异常信息

System.out.println(m + " failed: " + exc);

}//如果抛异常表示注解使用错误,不应使用在非静态或带参数的方式上

catch (Exception exc) {

//打印测试未通过的方法信息

System.out.println("使用@Test注解错误的方法 : " + m);

}

}

// ExceptionTest注解实现工具

if (m.isAnnotationPresent(ExceptionTest.class)) {

tests++;

try {

m.invoke(null);

//如果注解工具运行到这里,则测试方法未抛出异常,但属于测试未通过

System.out.printf("Test %s failed: no exception%n", m);

catch (Throwable wrappedExc) {

//获取异常根源

Throwable exc = wrappedExc.getCause();

//取出注解的值

Class<? extends Exception>[] excTypes = m.getAnnotation(

ExceptionTest.class).value();

int oldPassed = passed;

//将根源异常与注解值对比

for (Class<? extends Exception> excType : excTypes) {

//如果测试方法抛出的异常与注解中预期的异常匹配则表示测试通过

if (excType.isInstance(exc)) {//使用动态的instance of

passed++;

break;

}

}

//打印测试没有通过的方法信息

if (passed == oldPassed)

System.out.printf("Test %s failed: %s %n", m, exc);

}

}

}

//打印最终测试结果,通过了多少个,失败了多少个

System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);

}

}

下面是两次运行输出结果(前面的为测试Sample类,后面是测试Sample1类):

public static void Item35.Sample.m3() failed: java.lang.RuntimeException: Boom

使用@Test注解错误的方法 : public void Item35.Sample.m5()

public static void Item35.Sample.m7() failed: java.lang.RuntimeException: Crash

Passed: 1, Failed: 3

Test public static void Item35.Sample2.m2() failed: java.lang.ArrayIndexOutOfBoundsException: 1

Test public static void Item35.Sample2.m3() failed: no exception

Passed: 2, Failed: 2

本条目中开发的测试框架只是一个试验,但它清楚地示范了注解优于命名模式,这只是揭开注解功能的冰山一角。如果在编写一个需要程序员给源文件添加信息的工具,就要定义一组适当的注解类型。另外,我们要考虑使用Java平台提供的预定义的标准注解类型(见第36条)。

36、      坚持使用Override注解

当你打算重写一个方法时,有可能写成重载,这时如果@Override注解就可以防止出现这个问题。

这个经典示例涉及equals方法,程序员可以编写如下代码:

public boolean equals (Foo that){…}

当你应当把他们编写成如下时:

public Boolean equals(Object that)

这也是合法的,但是类Foo从Object继承了equals实现,最终成了重载,而原本是重写的,这时我们可以使用@Override在重写的方法前,这样如果在没有重写的情况下,编译器则会提示我们。

注,@Override不能用在实现父接口中的方法前面,因为这叫实现不叫重写了,这与可以加在实现父抽象类中方法前是不一样的。

总之,如果在你想要的每个方法声明中使用Override注解来覆盖超类声明,编译器可以替你防止大量的错误,但有一个例外,在具体的类中,不必标注你确认覆盖了抽象方法声明的方法,虽然这么做也没有什么坏处。

37、      用标记接口定义类型

标记接口是没有包含方法声明的接口,而只是指明(或者“标明”)一个类实现了具有某种属性的接口。如,Serializable接口,通过实现这个接口,表明它的实例可以被写到ObjectOutputStream中(或者“被序列化”)。

标记注解,没有参数,只是“标注”被注解的元素,如果第 35 条的@Test就是一个标记注解。

标记注解并不能替代标记接口。标记接口有两点胜过标记注解。最重要的一点是,标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。标记接口在编译时就会被检测是否有错误,而标记注解则要到运行时期(但也并非如此,就Serializable标记接口而言,如果它的参数没有实现该接口,ObjectOutputStream.write(Object)方法将会失败,但令人不解的是,ObjectOutputStream API的创建者在声明Write方法时并没有利用Serializable接口,该方法的参数类型应该为Serializable而非Object,因此,试着在没有实现Serializable的对象上调用ObjectOutputStream.write,只会在运行时出错,所以也并不是像前面说的那样)。

HYPERLINK "mailto:如果你正在编写的是目标为@Target(ElementType.TYPE)" 如果你正在编写的是目标为@Target(ElementType.TYPE)的标记注解类型,就要考虑使用标记接口来实现呢。

总之,接口是用来定义类型的,而注解是用来辅助分析类元素信息的。

从某种意义上说,本条目与第19条中“如果不想定义类型就不要使用接口”的说法相反。本科目最接近的意思是说:“如果想要定义类型,一定要使用接口”。

[Effective Java]第六章 枚举和注解的更多相关文章

  1. 《Effective Java》学习笔记 —— 枚举、注解与方法

    Java的枚举.注解与方法... 第30条 用枚举代替int常量 第31条 用实例域代替序数 可以考虑定义一个final int 代替枚举中的 ordinal() 方法. 第32条 用EnumSet代 ...

  2. Effective Java 读书笔记之五 枚举和注解

    Java1.5中引入了两个新的应用类型家族,新的类为枚举类型,新的接口为注解类型. 一.用enum代替int常量 1.枚举值由一组固定的常量组成合法值的类型. 二.用实例域代替序数 1.不要根据枚举的 ...

  3. EFFECTIVE JAVA 第十一章 系列化

    EFFECTIVE  JAVA  第十一章  系列化(将一个对象编码成一个字节流) 74.谨慎地实现Serializable接口 *实现Serializable接口付出的代价就是大大降低了“改变这个类 ...

  4. “全栈2019”Java第六章:注释

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  5. 编写高质量代码:改善Java程序的151个建议(第6章:枚举和注解___建议88~92)

    建议88:用枚举实现工厂方法模式更简洁 工厂方法模式(Factory Method Pattern)是" 创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其它子类" ...

  6. 第8章 枚举类&注解

    8.枚举及注解 8.1 如何自定义枚举类 1课时 8.2 如何使用关键字enum定义枚举类 1课时 8.3 枚举类的主要方法 1课时 8.4 实现接口的枚举类 1课时 8-1 枚举类 枚举类入门 枚举 ...

  7. effective java 第2章-创建和销毁对象 读书笔记

    背景 去年就把这本javaer必读书--effective java中文版第二版 读完了,第一遍感觉比较肤浅,今年打算开始第二遍,顺便做一下笔记,后续会持续更新. 1.考虑用静态工厂方法替代构造器 优 ...

  8. Java 第六章

    第六章 for语法:for(表达式①;表达式②;表达式③){ //④循环操作}表达式含义:表达式1:赋值语句, 它用来给循环变量赋初值 例如:int i = 1;表达式2:循环条件,一个关系表达式, ...

  9. Effective Java - 构造器私有、枚举和单例

    目录 饿汉式单例 静态常量 静态代码块 懒汉式单例 尝试加锁 同步代码块 双重检查 静态内部类单例 枚举单例 Singleton 是指仅仅被实例化一次的类.Singleton代表了无状态的对象像是方法 ...

随机推荐

  1. android 学习随笔十九(对话框、样式、主题、国际化 )

    1.对话框 package com.itheima.dialog; import android.os.Bundle; import android.app.Activity; import andr ...

  2. Spring 复习笔记01

    Spring 框架 1. core:整个Spring框架构建在Core核心模块上,它是整个框架的的基础. 2. AOP:AOP模块提供了一个轻便但功能强大强大的AOP框架,让我们可以以AOP的形式增强 ...

  3. 【secureCRT】中文乱码问题

    Options->Session Options->Appearance->Font->新宋体 字符集:中文GB2312 ->Character encoding 为UT ...

  4. WordPress博客网站fonts.useso加载慢解决办法

    WordPress博客网站fonts.useso加载慢解决办法 之前WordPress博客因为google字体库访问不了替换成360的useso,最近WordPress博客网站一直等待fonts.us ...

  5. JSP/Servlet 中的汉字编码问题

    JSP/Servlet 中的汉字编码问题 1.问题的起源 每个国家(或区域)都规定了计算机信息交换用的字符编码集,如美国的 ASCII,中国的 GB2312 -80,日本的 JIS 等,作为该国家/区 ...

  6. spring命名空间不需要版本号

    为什么dubbo启动没有问题? 这篇blog源于一个疑问: 我们公司使了阿里的dubbo,但是阿里的开源网站http://code.alibabatech.com,挂掉有好几个月了,为什么我们的应用启 ...

  7. bash环境变量读取顺序

    bash环境变量读取顺序: 交互式登录的用户: /etc/profile --> /etc/profile.d/*.sh --> ~/.bash_profile --> ~/.bas ...

  8. OpenCV学习 物体检测 人脸识别 填充颜色

    介绍 OpenCV是开源计算机视觉和机器学习库.包含成千上万优化过的算法.项目地址:http://opencv.org/about.html.官方文档:http://docs.opencv.org/m ...

  9. ectouch第六讲 之表常用链接

    ECTouch1.0 常用链接:精品属性商品mobile/index.php?m=default&c=category&type=best 新品属性商品mobile/index.php ...

  10. Java异常捕获之try-catch-finally-return的执行顺序-转载

    情况1:try块中没有抛出异常try和finally块中都有return语句 public static int NoException(){ int i=10; try{ System.out.pr ...