Effective java笔记(五),枚举和注解
30、用enum代替int常量
枚举类型是指由一组固定的常量组成合法值的类型。在java没有引入枚举类型前,表示枚举类型的常用方法是声明一组不同的int常量,每个类型成员一个常量,这种方法称作int枚举模式。采用int枚举模式的程序是十分脆弱的,因为int值是编译时常量,若与枚举常量关联的int发生变化,客户端就必须重新编译。
java枚举类型背后的思想:通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。客户端既不能创建枚举类型的实例,也不能对它进行扩展。枚举类型是实例受控的,它们是单例的泛型化,本质上是单元素的枚举。枚举提供了编译时的类型安全。
包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间。可以增加或重新排列枚举类型中的常量,而无需重新编译客户端代码,因为常量值并没有被编译到客户端代码中。可以调用toString方法,将枚举转换成可打印的字符串。
枚举类型允许添加任意的方法和域,并实现任意的接口。枚举类型默认继承Enum类(其实现了Comparable、Serializable接口)。为了将数据与枚举常量关联起来,得声明实例域,并编写一个将数据保存到域中的构造器。枚举天生就是不可变的,所有的域都必须是final的。
例如:
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; //质量kg
private final double radius; //半径
private final double surfaceGravity; //表面重力,final常量构造器中必须初始化
private static final double G = 6.673E-11;
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;
}
//测试
public static void main(String[] args) {
double earthWeight = 175;
double mass = earthWeight/Planet.EARTH.surfaceGravity();
for(Planet p : Planet.values()) {
//java的printf方法中换行用%n, C语言中用\n
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}
}
上面的方法对大多数枚举类型来说足够了,但有时你需要将本质上不同的行为与每个常量关联起来。这时通常需要在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主题中实现这个方法。这个方法被称作特定于常量的方法实现。例如:
public enum Operation {
PULS("+") {
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);
public static void main(String[] args) {
double x = 2.0;
double y = 4.0;
for(Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x,y));
}
}
枚举类型中的抽象方法,在它的常量中必须被实现。除了编译时常量之外,枚举构造器不可以访问枚举的静态域,因为构造器运行时,静态域还没被初始化。
特定于常量的方法,使得在枚举常量中共享代码变的更加困难。例如:根据给定的工人的基本工资(按小时算)和工作时间,用枚举计算工人当天的工作报酬。其中加班工资为平时的1.5倍。
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURADAY, FRIDAY, SATURDAY, SUNDAY;
private static final int HOURS_PER_SHIFT = 8;
double pay(double hoursWorked, double payRate) {
switch(this) {
case SATURDAY: case SUNDAY :
return hoursWorked*payRate*1.5;
default :
return hoursWorked - HOURS_PER_SHIFT > 0
?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate)
: hoursWorked*payRate;
}
}
public static void main(String[] args) {
System.out.println(PayrollDay.MONDAY.pay(10,10));
System.out.println(PayrollDay.SUNDAY.pay(10,10));
}
}
上面这段代码虽然十分简洁,但是维护成本很高。每将一个元素添加到该枚举中,就必须修改switch语句。可以使用策略枚举来进行优化,例如:
public enum PayrollDay {
MONDAY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURADAY(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 pay(double hoursWorked, double payRate) {
return hoursWorked - HOURS_PER_SHIFT > 0
?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate)
: hoursWorked*payRate;
}
},
WEEKEND {
double pay(double hoursWorked, double payRate) {
return hoursWorked * payRate * 1.5;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double pay(double hoursWorked, double payRate);
}
public static void main(String[] args) {
System.out.println(PayrollDay.MONDAY.pay(10,10));
System.out.println(PayrollDay.SUNDAY.pay(10,10));
}
}
总之,与int常量相比,枚举类型优势明显。许多枚举都不需要显式的构造器或成员。当需要将不同的行为与每个常量关联起来时,可使用特定于常量的方法。若多个枚举常量同时共享相同的行为,考虑使用策略枚举。
31、用实例域代替序数
所有的枚举都有一个ordinal方法,它返回枚举常量在类中的位置。若常量进行重排序,它们ordinal的返回值将发生变化。所以,永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中。
public enum Planet {
MERCURY(1),
VENUS(2),
EARTH(3),
MARS(4),
JUPITER(5),
SATURN(6),
URANUS(7),
NEPTUNE(8);
private final int numOrd;
Planet(int numOrd) {this.numOrd = numOrd; }
public int numOrd(){ return numOrd; }
}
Enum规范中谈到ordinal时写道:它是用于像EnumSet和EnumMap这种基于枚举的数据结构的方法,平时最好不要使用它。
32、用EnumSet代替位域
若枚举类型要用在集合中,可以使用EnumSet类。EnumSet类是专为枚举类设计的集合类,EnumSet中的所有元素都必须是单个枚举类型中的枚举值。若元素个数小于64,整个EnumSet就用一个long来表示,所以它的性能比的上位域(通过位操作实现)的性能。
import java.util.*;
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) {
// Body goes here
}
// Sample use
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
EnumSet类集位域的简洁和性能优势及枚举的所有优点与一身。y应使用EnumSet代替位域操作。
33、用EnumMap代替序数索引
EnumMap是一种键值必须为枚举类型的映射表。虽然使用其它的Map实现(如HashMap)也能完成枚举类型实例到值的映射,但是使用EnumMap会更加高效。由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值,这使得EnumMap的效率比其它的Map实现(如HashMap也能完成枚举类型实例到值的映射)更高。
注意:EnumMap在内部使用枚举类型的ordinal()得到当前实例的声明次序,并使用这个次序维护枚举类型实例对应值在数组中的位置。
例如:
import java.util.*;
public class DatabaseInfo {
private enum DBType { MYSQL, ORACLE, SQLSERVER }
private static final EnumMap<DBType, String> urls
= new EnumMap<>(DBType.class);
static {
urls.put(DBType.MYSQL, "jdbc:mysql://localhost/mydb");
urls.put(DBType.ORACLE, "jdbc:oracle:thin:@localhost:1521:sample");
urls.put(DBType.SQLSERVER, "jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=mydb");
}
private DatabaseInfo() {}
public static String getURL(DBType type) {
return urls.get(type);
}
public static void main(String[] args) {
System.out.println(DatabaseInfo.getURL(DBType.SQLSERVER));
System.out.println(DatabaseInfo.getURL(DBType.MYSQL));
}
}
不要用序数(ordinal方法)来索引数组,而要使用
EnumMap。若所表示的关系是多维的,可以使用EnumMap<.., EnumMap<..>>。一般情况下不要使用Enum.ordinal。
34、用接口模拟可伸缩的枚举
枚举类型都默认继承自java.lang.Enum类。虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,以实现对程序的扩展。
例如:
import java.util.*;
//测试
public class Test {
public static <T extends Enum<T> & Operation> void test(
Class<T> opSet, double x, double y) {
for(Operation op : opSet.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x,y));
}
}
public static void main(String[] args) {
double x = 2.0;
double y = 4.0;
test(ExtendedOperation.class, x, y);
test(BasicOperation.class, x, y);
}
}
//接口
interface Operation {
double apply(double x, double y); //默认为public的
}
//基本操作,实现Operation接口
enum BasicOperation implements Operation{
PULS("+") {
//访问权限必须为public,否则报错
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; }
}
//扩展操作,实现Operation接口
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; }
}
其中<T extends Enum<T> & Operation>确保了Class对象既是枚举类型又是Operation的子类型。
35、注解优先于命名模式
java1.5之前,一般使用命名模式来对程序进行特殊处理。如,JUnit测试框架要求用test作为测试方法名称的开头。使用命名模式有几个缺点:
- 文字拼写错误会导致失败,且没有任何提示。如,test写成tset
- 无法确保它们只用于相应的程序元素上。如,变量名使用test开头
- 没有提供将参数值与程序元素关联起来的方法
注解很好的解决了这些问题。关于注解的详细用法请看 java基础(二),Annotation(注解)
例如:
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ExceptionTest {
Class<? extends Exception>[] value();
}
class Sample {
@ExceptionTest( { IndexOutOfBoundsException.class,
NullPointerException.class})
public static void doublyBad() {
//List<String> list = new ArrayList<>();
List<String> list = null;
list.add(5,null);
}
}
public class RunTest {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("Sample");
for(Method m : testClass.getDeclaredMethods()) {
if(m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try{
m.invoke(null);
}catch (InvocationTargetException ite) {
//Throwable exc = ite.getTargetException();
Throwable exc = ite.getCause();
Class<? extends Exception>[] excTypes
= m.getAnnotation(ExceptionTest.class).value();
for(Class<? extends Exception> excType : excTypes) {
if(excType.isInstance(exc)) {
excType.newInstance().printStackTrace();
}
}
}
}
}
}
}
在利用 Method 对象的 invoke 方法调用目标对象的方法时, 若在目标对象的方法内部抛出异常, 会抛出 InvocationTargetException 异常, 该异常包装了目标对象的方法内部抛出异常, 可以通过调用 InvocationTargetException 异常类的的 getTargetException() 方法得到原始的异常.
在编写一个需要程序员给源文件添加信息的工具时,应该定义一组适当的注解,而不是使用命名模式。
36、坚持使用Override注解
@Override注解只能用在方法声明中,它表示被注解的方法声明覆盖了超类型中的一个声明。使用Override注解可以有效防止覆盖方法时的错误。
例如:想要在String中覆盖equals方法
//这是方法重载,将产生编译错误
@Override
public boolean equals(String obj) {
....
}
//覆盖
@Override
public boolean equals(Object obj) {
....
}
37、用标记接口定义类型
标记接口是指没有任何属性和方法的接口,它只用来表明类实现了某种属性。如,Serializable接口,通过实现这个接口,类表明它的实例可以被写到ObjectOutputStream。标记注解是特殊类型的注解,其中不包含成员。标记注解的唯一目的就是标记声明。
- 标记接口的优点:标记接口允许你在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误。
- 标记注解的优点:便于扩展,可以给已被使用的注解类型添加更多信息(元注解)。而接口实现后不可能再添加方法。
标记接口与标记注解如何选择:
- 标记接口和标记注解都各有好处。若想要定义一个任何新方法都不会与之关联的类型,就是用标记接口。若想要标记成员元素而非类和接口(方法或成员变量)或未来可能要给标记添加更多信息,就使用标记注解。
Effective java笔记(五),枚举和注解的更多相关文章
- Effective Java笔记一 创建和销毁对象
Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参数时要考虑用构建器 第3条 用私有构造器或者枚举类型强化Singleton属性 第4条 ...
- Effective java笔记(二),所有对象的通用方法
Object类的所有非final方法(equals.hashCode.toString.clone.finalize)都要遵守通用约定(general contract),否则其它依赖于这些约定的类( ...
- effective java笔记之单例模式与序列化
单例模式:"一个类有且仅有一个实例,并且自行实例化向整个系统提供." 单例模式实现方式有多种,例如懒汉模式(等用到时候再实例化),饿汉模式(类加载时就实例化)等,这里用饿汉模式方法 ...
- Effective Java 第三版——39. 注解优于命名模式
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- effective java笔记之java服务提供者框架
博主是一名苦逼的大四实习生,现在java从业人员越来越多,面对的竞争越来越大,还没走出校园,就TM可能面临失业,而且对那些增删改查的业务毫无兴趣,于是决定提升自己,在实习期间的时间还是很充裕的,期间自 ...
- Effective Java 读书笔记之五 枚举和注解
Java1.5中引入了两个新的应用类型家族,新的类为枚举类型,新的接口为注解类型. 一.用enum代替int常量 1.枚举值由一组固定的常量组成合法值的类型. 二.用实例域代替序数 1.不要根据枚举的 ...
- 《Effective Java》读书笔记 - 6.枚举和注解
Chapter 6 Enums and Annotations Item 30: Use enums instead of int constants Enum类型无非也是个普通的class,所以你可 ...
- Effective java笔记5--通用程序设计
一.将局部变量的作用域最小化 本条目与前面(使类和成员的可访问能力最小化)本质上是类似的.将局部变量的作用域最小化,可以增加代码的可读性和可维护性,并降低出错的可能性. 使一个局部变量的作用 ...
- Effective java笔记(七),通用程序设计
45.将局部变量的作用域最小化 将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性. Java允许在任何可以出现语句的地方声明变量(C语言中局部变量要在代码块开头声明),要使 ...
随机推荐
- poj3122-Pie(二分法+贪心思想)
一,题意: 有f+1个人(包括自己),n块披萨pie,给你每块pie的半径,要你公平的把尽可能多的pie分给每一个人 而且每个人得到的pie来自一个pie,不能拼凑,多余的边角丢掉.二,思路: 1,输 ...
- ABP理论学习之设置管理
返回总目录 本篇目录 介绍 定义设置 获取设置值 更改设置 关于缓存 介绍 每个应用程序都需要存储一些设置信息,然后在应用程序中的某个地方使用这些设置.ABP提供了健壮的基础设施来存储或检索服务端和客 ...
- CoreData教程
网上关于CoreData的教程能搜到不少,但很多都是点到即止,真正实用的部分都没有讲到,而基本不需要的地方又讲了太多,所以我打算根据我的使用情况写这么一篇实用教程.内容将包括:创建entity.创建r ...
- 探索C#之微型MapReduce
MapReduce近几年比较热的分布式计算编程模型,以C#为例简单介绍下MapReduce分布式计算. 阅读目录 背景 Map实现 Reduce实现 支持分布式 总结 背景 某平行世界程序猿小张接到B ...
- [.net 面向对象程序设计进阶] (16) 多线程(Multithreading)(一) 利用多线程提高程序性能(上)
[.net 面向对象程序设计进阶] (16) 多线程(Multithreading)(一) 利用多线程提高程序性能(上) 本节导读: 随着硬件和网络的高速发展,为多线程(Multithreading) ...
- 仅此一文让你明白ASP.NET MVC 之View的显示(仅此一文系列二)
题外话 一周之前写的<仅此一文让你明白ASP.NET MVC原理>受到了广大学习ASP.NET MVC同学的欢迎,于是下定决心准备把它写成一个系列,以满足更多求知若渴的同学们.蒋金楠老师已 ...
- 每天一个linux命令(23):Linux 目录结构
对于每一个Linux学习者来说,了解Linux文件系统的目录结构,是学好Linux的至关重要的一步.,深入了解linux文件目录结构的标准和每个目录的详细功能,对于我们用好linux系统只管重要,下面 ...
- Redux
redux是Flux的一种实现方式,但还是和Flux有些不同. React控制视图层,要想做一个完整的数据流,必须要用react-redux. 官方demo,自己收集了一下: demo1http:// ...
- JAVA基础-输入输出:1.编写TextRw.java的Java应用程序,程序完成的功能是:首先向TextRw.txt中写入自己的学号和姓名,读取TextRw.txt中信息并将其显示在屏幕上。
1.编写TextRw.java的Java应用程序,程序完成的功能是:首先向TextRw.txt中写入自己的学号和姓名,读取TextRw.txt中信息并将其显示在屏幕上. package Test03; ...
- 使用Event Message 对 Package 进行Troubleshoot
在SSIS Server上,发现一个Package Job运行异常,该Package处于僵死状态.从 Job Activity Monitor中看到该Job一直处于运行状态,但是,DW中没有执行任何Q ...