主要讨论枚举和注解
第二十六条 用enum代替int常量
1.枚举没有公有的可以访问的构造器,客户端不能创建枚举类型的实例,也不能对它进行扩展,继承什么的,
只有使用枚举类型里面声明过的枚举常量,所以说枚举是实例受控的,也可以说每个枚举常量都是一个枚举类型的
一个实例
2.枚举保证了编译时的类型安全,例如声明参数的类型是Apple,那么传入给这个参数的对象一定是Apple枚举类型定义的
常量
3.枚举类型的常量值没有被编译到客户端代码中
4.枚举类型可以添加任意的方法和域,并实现任意接口,提供了Object方法的高级实现,实现了Comparable 和Serializable
5.为了将数据和枚举常量关联起来,得声明实例域(所有域都是final的,最好是私有的,并提供公有的访问方法),并编写
一个带有数据并将数据保存在实例域的构造器
6.values()方法,按照声明顺序返回枚举声明的常量值的数组;toString()返回每个枚举值的声明名称
7.当把一个元素从一个枚举类型中删除,没有引用这个元素的客户端继续正常工作,引用的客户端,没有重新编译客户端代码
会在运行到这一行会抛出异常,编译客户端代码会编译出错,提醒不存在这个元素
8.例子:
public enum OperationOne {
PLUS,MINUS,TIMES,DIVIDE;
public double apply(double x,double y){
switch (this){
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x/ y;
}
throw new AssertionError("unknown op"+this);
}
}
//上面的例子的弊端是,添加新的枚举常量,可能忘记添加对应的apply,下面是改良
public enum OperationTwo {
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;
}
};
public abstract double apply(double x,double y);
}
//可以添加关联的符号,覆盖toString方法
private final String symbol;
OperationThree(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return this.symbol;
}
//还可以提供静态的一个map来保存对应关系的数据,然后通过提供一个公有的静态方法根据符号返回对应的枚举常量
private final static Map<String,OperationFour> stringToEnum = Stream.of(values()).collect(Collectors.toMap(Object::toString,e->e));
public static Optional<OperationFour> fromString(String symbol){
return Optional.ofNullable(stringToEnum.get(symbol));
}
9.枚举构造器不能访问枚举的静态域,因为构造器运行的时候,这些静态域还没初始化
10.策略枚举的列子
public enum PayrollDayOne {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked,int payRate){
int basePay = minutesWorked * payRate;
int overtimePay;
switch (this){
case SATURDAY: case SUNDAY: overtimePay =basePay /2;
break;
default: overtimePay = minutesWorked <= MINS_PER_SHIFT?0:(minutesWorked-MINS_PER_SHIFT) * payRate/2;
}
return basePay + overtimePay;
}
}
//上面的代码,非常危险,添加新的特殊的节日,计算公式还是按正常的计算,可以使用策略枚举来去掉switch
public enum PayrollDayTwo {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY(PayType.WEEKEND),SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDayTwo(PayType payType) {
this.payType = payType;
}
PayrollDayTwo() {
this.payType = PayType.WEEKDAY;
}
int pay(int minutesWorked,int payRate){
return this.payType.pay(minutesWorked,payRate);
}
private enum PayType{
WEEKDAY {
@Override
int overtimePay(int mins, int payRate) {
return (mins-MINS_PER_SHIFT) * payRate /2;
}
},
WEEKEND{
@Override
int overtimePay(int mins, int payRate) {
return mins * payRate * 2;
}
};
private static final int MINS_PER_SHIFT = 8 * 60;
abstract int overtimePay(int mins,int payRate);
int pay(int minutesWorked,int payRate){
int basePay = minutesWorked * payRate;
return basePay + overtimePay(minutesWorked,payRate);
}
}
}
11.枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为
//如果OperationOne不是你能控制的,你需要返回一个操作的反操作,可以编写这样的代码
public static OperationOne inverse(OperationOne operationOne){
switch (operationOne){
case PLUS:return OperationOne.MINUS;
case MINUS:return OperationOne.PLUS;
case TIMES:return OperationOne.DIVIDE;
case DIVIDE:return OperationOne.TIMES;
default: throw new AssertionError("unknown op"+operationOne);
}
}
12.每当需要一组固定常量,并在编译时就知道其成员的时候,就应该使用枚举
13.枚举类型中的常量集并不一定要始终保持不变
第三十五条,用实例域代替序数(ordinal()的注意事项)
1.许多枚举天生就与一个单独的int值相关联,所有的枚举都有一个ordinal方法,返回每个枚举常量在类型中的数字位置,
从零开始,它的设计用于像EnumSet和EnumMap这种基于枚举的通用的数据结构
2.永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例中
public enum EnsembleOne {
SOLO,DUET;
public int getNumberOfMusicians() {
return ordinal()+1;
}
}
//上面的ordinal()位置改变就会改变,很危险
public enum Ensemble {
SOLO(1),DUET(2);
//使用实例域替换ordinal
private final int numberOfMusicians;
Ensemble(int numberOfMusicians) {
this.numberOfMusicians = numberOfMusicians;
}
public int getNumberOfMusicians() {
return numberOfMusicians;
}
}
第三十六,用EnumSet代替位域
1.位域是什么,很简单就是使用位,一个字节8位,那么可以表示8个状态,比如00000001,表示东;00000010,表示南;
00000100,表示西, 00001000,表示北。位域(|)操作符,只有有一个为1那就是1,当 东 | 北 =00000001 | 00001000 = 00001001
2.EnumSet根据elements |= (1L << ((Enum<?>)e).ordinal());来保存元素的,性能并不比位域差,而且操作安全,提供好用的api,缺点是无法
创建一个不可变的EnumSet
private enum Style{ BOLE,ITALIC,UNDERLINE,STRIKETHROUGH}
public static void main(String[] args){
final EnumSet<Style> styleEnumSet = EnumSet.of(Style.BOLE, Style.UNDERLINE);
//不可变
final Set<Style> styles = Collections.unmodifiableSet(styleEnumSet);
final boolean add = styles.add(Style.ITALIC);
}
第三十七,用EnumMap代替序数索引
1.最好不要使用序数(ordinal)来索引数组,而要使用EnumMap,多维使用EnumMap<...,EnumMap<...,...>>
2.例子
public enum Phase {
SOLID,LIQUID,GAS;
public enum Transition{
MELT(SOLID,LIQUID),FREEZE(LIQUID,SOLID);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
//有意思啊
private static final Map<Phase,Map<Phase,Transition>> m = Stream.of(values()).collect((Collectors.groupingBy(t->t.from,
()->new EnumMap<>(Phase.class),Collectors.toMap(t->t.to,t->t,(x,y)->y,()->new EnumMap<>(Phase.class)))));
public static Transition from(Phase from,Phase to){
return m.get(from).get(to);
}
}
}
第三十八条,用接口模拟可扩展的枚举(面向接口编写枚举)
1.<T extends Enum<T> & Operation> 既是Enum 又实现 Operation
2.虽然无法编写可扩展的枚举,却可以通过编写接口以及实现该接口的基础枚举来对它进行模拟
3.例子
public interface Operation {
public double apply(double x, double y);
}
//实现接口的枚举
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;
}
}
//使用
public class MainTest {
public static void main(String[] args){
test(ExtendedOperation.class,5,6);
}
public static <T extends Enum<T> & Operation> void test(Class<T> opEnumType,double x , double y){
for (Operation op:opEnumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));
}
}
}
第三十九条,注解优先于命名模式
1.既然有了注解,就完全没有理由再使用命名模式了
1.命名模式:也就是约定名字的含义,比如约定方法名字必须是test_开头才能使用
2.注解例子
//最简单的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
//带一个参数的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
Class<? extends Throwable> value();
}
//带多个参数的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
Class<? extends Throwable>[] value();
}
//解析程序
public static void parseTestAnnotation(Class<?> clazz){
int tests = 0;
int passed = 0;
for (Method method : clazz.getDeclaredMethods()) {
if(method.isAnnotationPresent(Test.class)){
tests++;
try {
method.invoke(null);
passed++;
}catch (InvocationTargetException inv) {
final Throwable cause = inv.getCause();
final Class<? extends Throwable>[] value = method.getAnnotation(Test.class).value();
for (Class<? extends Throwable> aClass : value) {
if(aClass.isInstance(cause)){
passed++;
break;
}
}
} catch (Exception e) {
System.out.println("Invalid @Test:"+method);
}
}
}
System.out.printf("Passed: %d,Failed:%d%n",passed,tests-passed);
}
//组合注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TestContainer {
Test[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(TestContainer.class) //重点
public @interface Test {
Class<? extends Throwable>[] value();
}
//解析
//注意isAnnotationPresent只能判断一个,需要判断TestContainer和Test
if (m.isAnnotationPresent(TestContainer.class) || m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (IllegalAccessException inv) {
final Throwable cause = inv.getCause();
//getAnnotationsByType既可以判断TestContainer和Test
for (Test test : m.getAnnotationsByType(Test.class)) {
for (Class<? extends Throwable> aClass : test.value()) {
if(aClass.isInstance(cause)){
passed++;
}
}
}
} catch (InvocationTargetException e) {
System.out.println("Invalid @Test:"+m);
}
}
2.所有的程序员都应该使用java平台所提供的预定义的注解类型
第四十条,坚持使用Override注解(编译器会提醒你,防止你没有正确的覆盖父类的方法)
1.被注解的声明覆盖了超类型中的一个方法声明
2.在你想要覆盖超类声明的每个方法声明中使用Override注解
3.如果你在编写一个没有标注为抽象的类,并确保它覆盖了超类的超类的抽象方法,在这种情况下,就
可不必将Override注解放在改方法上,(但我自己觉得还是标上,可以很轻松的知道这个类覆盖了什么方法)
第四十一条,用标记接口定义类型(不包含方法声明的接口,Serializable)
1.标记接口:不包含方法声明的接口
2.标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型,标记接口的存在,允许
你在编译时就能捕捉到在使用标记注解的情况下要到运行时才能捕捉的错误,比如需要传入实现Serializable的对象
才能序列化,接收参数应该是Serializable类型的
3.标记接口胜过标记注解的另一个优点,他们可以被更加精确的进行锁定
4.标记接口胜过标记注解最大的优点在于,它们是更大的注解机制的一部分
5.如果你发现自己在编写的是@Target(ElementType.type)的标记注解,就花点时间考虑清楚,他是否真的应该为注解类型,
想想标记接口是否合适
6.如果可以标记的地方不单单是类和接口,那么注解最合适,但编写一个特殊的接口(Serializable),可能标记接口
更合适
网友评论