Java - 枚举与注解
Enumeration
于Java 1.5增加的enum type...
enum type是由一组固定的常量组成的类型,比如四个季节、扑克花色。
在出现enum type之前,通常用一组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 i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;
虽合法但诧异,这是在做果汁吗?
而且,这种常量是compile-time常量,编译后一切都结束了,使用这个常量的地方都被替换为该常量的值。
如果该常量值需要改变,所有使用该常量的代码都必须重新编译。
更糟糕的情况是,不重新编译也可以正常运行,只不过会得到无法预测的结果。
(ps:我觉得更遭的是有人直接把常量值写到代码里...)
另外,比如上面的APPLE_FUJI,我想打印它的名字,不是它的值。
不仅如此,我还想打印所有苹果,我想打印苹果一共有多少种类。
当然,如果想打印也可以,只是相比直接使用enum,无论怎么做都很麻烦。
如果使用enum,比如:
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
看起来就是一堆常量,但是enum没有实例,也没有可访问的构造器,无法对其进行扩展。
enum本身就是final,所以很多时候也直接用enum实现singleton。
enum在编译时是类型安全的,比如有地方声明了上面代码中的Apple类型的参数,那么被传到该参数的引用肯定是三种苹果之一。
而且enum本身就是一个类型,可以有自己的方法和field,而且可以实现接口。
附上书中太阳系enum,很难想象如果有类似需求时用普通常量来实现。
也许我可以声明一个Planet类,再给它加上field的方法,然后在一个constant类中声明为final
但这样却无法保证Planet类仅用作常量,所以还是用enum吧:
public enum Planet {
MERCURY(3.302e+23, 2.439e6), VENUS(4.869e+24, 6.052e6), EARTH(5.975e+24,
6.378e6), MARS(6.419e+23, 3.393e6), JUPITER(1.899e+27, 7.149e7), SATURN(
5.685e+26, 6.027e7), URANUS(8.683e+25, 2.556e7), NEPTUNE(1.024e+26,
2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() {
return mass;
}
public double radius() {
return radius;
}
public double surfaceGravity() {
return surfaceGravity;
}
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
然后我们就可以这样使用Planet enum,无论是值还是名字,使用起来都很自然:
public class WeightTable {
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Weight on %s is %f%n",p, p.surfaceWeight(mass));
}
}
其实像Planet这样的方式对多数使用枚举的场景而言足够了。
也就是说每个Planet常量表达的是不同的数据,但也有例外。
比如,我们要为enum中的每一个常量赋予不同的行为。
下面是书中用enum表达计算的例子:
import java.util.HashMap;
import java.util.Map;
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) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
abstract double apply(double x, double y);
private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();
static {
for (Operation op : values())
stringToEnum.put(op.toString(), op);
}
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));
}
}
对不同的枚举常量进行switch..case..其实也能表达出我们想要的效果。
如果以后增加了新的常量则需要再对应加上一个case,当然,不加也不会有任何提示,然后最坏的情况就是运行时出了问题。
如上面的代码是常量行为的正确使用方法,即constant-specific method implementation。
为行为提供一个抽象,并为每一个常量提供一个实现,即一个枚举常量也是constant-specific class body。
采用这种方式时,如果新增一个常量,则必须提供一个方法实现,否则编译器会给出提示,这就多了一层保障。
遗憾的是,这种方式也有缺陷。
比如我们有这样一个需求,计算某一天的薪水,这个某一天可以是一周中的某一天,也可能是某个节日,比如周一到周五使用相同的运算方式,周末另算,某节日另算。
也就是说我需要在枚举中声明代表周一到周日的常量,如果我继续使用之前的方式去声明一个抽象方法,如果周一到周五采用完全一样的计算,则会出现五段完全相同的代码。
但即使这样我们也不能用回switch..case..方式,增加一个常量时强制选择其选择一种行为实现是必须的。
于是我们有一种叫strategy enum的方式,即枚举中声明另外一个枚举的field,该field则代表策略,并提供策略相关的行为。
下面是书中代码:
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);
}
}
}
Annotation
在Java 1.5之前时常有这样的情况,通过为程序元素进行特殊的命名以提供特殊的功能,比如JUnit中测试方法必须为test开头。
当然,这种方式在某种程度上确实可行,但不够优雅。
比如:
- 错误的文字拼写并不会有任何提示,直到运行时才会发现出了问题。
- 其次,这种方式无法特指某个程序元素,比如用户将某个类名的开头做了特殊命名,希望作用于类中所有的方法,结果可能没有提示、没有效果、没有意义。
- 而且,这种方式太单调,比如我想和某个方法的参数或者和声明抛出的异常进行交互。当然,反射也可以,但问题是我如何在不知道用户行为的情况下提供反射方法。
平时工作很少提供过注解,大多数情况都是使用别人提供的注解。
没想过没有注解会是什么样子,但和naming pattern一比较发现确实太重要了。
比如在下面的例子,声明一个注解用于表示测试方法:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
//..
}
对于代码中的retention和target,我们有专门的术语叫做"元注解(meta-annotation)"。
而对于这种没有参数,仅仅标注程序元素的注解,我们称作"标记注解(marker annotation)"。
如果需要给注解声明参数并不复杂,只是相当于给一个类添加实例field。
如下代码,表示测试时发生异常数组中的异常时进行通过:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
当然,注解本身对程序元素并没有直接的影响,它无法改变代码本身的语义。
我们需要依赖于特定的注解处理类。
当然,并不是一个注解就对应一个处理类,一个处理类也可以处理很多种注解。
比如下面的代码为Test和ExceptionTest提供了处理:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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()) {
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("INVALID @Test: " + m);
}
}
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)) {
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);
}
}
代码就不多做解释了,主要是通过反射判断注解和获取异常。
其实标记注解非常常见,但说到标记注解就不得不说标记接口,比如Serializable什么的仅仅是作为注明。
相比接口只能在类名后面加上implements,注解可以作用于更多的程序元素。于是便得出结论,标记接口可以淘汰了?
但这样过于片面。
首先,被接口标记的类提供该接口的实现,而这一点是注解无法做到的,就算有处理类进行补助也无法成为一种约束。
就Serializable而言,如果被标记的类没有提供实现,ObjectOutputStream.write(Object)
则毫无意义。
另外,这个接口有点特殊,它确实是一种约束,但在编译期没给出警告。
我之前以为write方法没有定义在Serializable中可能有什么特殊意义,但作者原话是:
Inexplicably, the authors of the ObjectOutputStream API did not take advantage of the Serializable interface in declaring the write method.
可见他也不知道其中的意义,既然如此,我们也不仿效这种作法了吧。
第二点是接口标记地更加精确。
乍一看似乎有些矛盾,相比接口只能作用于类元素,注解可以作用于多种元素不是注解的优点吗?
其实作者表达的并不是这个观点,就一个接口和Target为ElementType.Type的注解而言,后者可以作用于任何类和接口。
作者用Set接口进行了说明,Set这种情况有些特殊,Set继承了Collection接口。
乍一看,Set似乎不是一个标记接口,它声明了太多方法。
参考:
The Set interface places additional stipulations, beyond those inherited from the Collection interface, on the contracts of all constructors and on the contracts of the add, equals and hashCode methods. Declarations for other inherited methods are also included here for convenience. (The specifications accompanying these declarations have been tailored to the Set interface, but they do not contain any additional stipulations.)
但作者将其描述为"a restricted marker interface",它声明的方法与Collection接口是相同的。
Set并没有改进Collection的契约,只是为实现类多提供了一种抽象描述。
但即便如此,也不能把注解设计成至少有一个参数的形式。
首先不得不承认,能标记的类型比接口更多,这个确实是一个优势。
另外,在一个类中,同一种标记注解可以出现多次,这一点也是其优势。
而最重要的,相比接口这种约定(即,声明后被一些类提供了实现,在后期版本中很难修改这个接口),注解则可以在后期变得更丰富。
Java - 枚举与注解的更多相关文章
- java枚举和注解
枚举 一.枚举(enumeration) 是一组常量的集合,可以理解为:枚举属于一种特殊的类,里面只包含一组有限的特定的对象,构造方法默认为private. 二.枚举的两种实现方式 1.自定义实现枚举 ...
- 编写高质量代码:改善Java程序的151个建议(第6章:枚举和注解___建议88~92)
建议88:用枚举实现工厂方法模式更简洁 工厂方法模式(Factory Method Pattern)是" 创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其它子类" ...
- Effective java笔记(五),枚举和注解
30.用enum代替int常量 枚举类型是指由一组固定的常量组成合法值的类型.在java没有引入枚举类型前,表示枚举类型的常用方法是声明一组不同的int常量,每个类型成员一个常量,这种方法称作int枚 ...
- [Effective Java]第六章 枚举和注解
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- 《Effective Java》学习笔记 —— 枚举、注解与方法
Java的枚举.注解与方法... 第30条 用枚举代替int常量 第31条 用实例域代替序数 可以考虑定义一个final int 代替枚举中的 ordinal() 方法. 第32条 用EnumSet代 ...
- Java复习——枚举与注解
枚举 枚举就是让某些变量的取值只能是若干固定值中的一个,否则编译器就会报错,枚举可以让编译器在编译阶段就控制程序的值,这一点是普通变量无法实现的.枚举是作为一种特殊的类存在的,使用的是enum关键字修 ...
- [Java读书笔记] Effective Java(Third Edition) 第 6 章 枚举和注解
Java支持两种引用类型的特殊用途的系列:一种称为枚举类型(enum type)的类和一种称为注解类型(annotation type)的接口. 第34条:用enum代替int常量 枚举是其合法值由一 ...
- Java基础(十)——枚举与注解
一.枚举 1.介绍 枚举类:类的对象只有有限个,确定的.当需要定义一组常量时,强烈建议使用枚举类.如果枚举类中只有一个对象,则可以作为单例模式的实现. 使用 enum 定义的枚举类默认继承了 java ...
- java 反射,注解,泛型,内省(高级知识点)
Java反射 1.Java反射是Java被视为动态(或准动态)语言的一个关键性质.这个机制允许程序在运行时透过Reflection APIs 取得任何一个已知名称的class的内部信息, 包括 ...
随机推荐
- Webpack vs Rollup
本文由作者余伯贤授权网易云社区发布. 2017年4月份的时候,Facebook将React的构建工具换成了Rollup.很多人就有疑问了,Webpack不也是Facebook团队开发的吗,为什么不使用 ...
- php中的XML DOM(11)
7.创建节点 在dom操作中,增删改操作必须要找父节点 1.DOMElement DOMDocument::createElement ( string $name [, string $value ...
- php中的XML DOM(10)
1.PHP DOM (1) Php中的DOM跟javascript不一样,属性不用另外增加一个节点 2.主要类 DOMDocument :文档类 DOMNodeList :节点列表类 DOMNode ...
- 从【BZOJ4173】谈做题技巧
题目描述 ----------------------------------------------------------------------------------------------- ...
- luoguP4647 [IOI2007] sails 船帆
https://www.luogu.org/problemnew/show/P4647 首先发现答案与顺序无关,令 $ x_i $ 表示高度为 $ i $ 的那一行帆的个数,第 $ i $ 行对答案的 ...
- 为IEnumerable类型添加Add方法
IEnumerable类型原生是没有Add方法的,你可以用Contact方法去为它添加元素, 1 items = items.Concat(new[] { "foo" }); 也可 ...
- CentOS6.5下samba服务
为减少错误已提前关掉了SELinux,防火墙. 安装rpm包: samba-3.6.9-164.el6.x86_64.rpm 启动检测:samba服务可以正常启动!(证明RPM安装正常) 配置文件位置 ...
- CentOS6.5下openssh服务
00×0 介绍 OpenSSH是使用SSH通过计算机网络加密通讯的实现.它是取代由SSH Communications Security所提供的商用版本的开放源代码方案.目前OpenSSH是OpenB ...
- jQuery的隔行换色
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...
- Hibernate中连接数据库的配置
Hibernate连接数据库的配置 实体类的映射文件 <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mappin ...