Effective java 总结5 - 枚举和注解

Effective java 总结5 - 枚举和注解

java两种特殊的引用类型

  • 类 :枚举类型
  • 接口 :注解类型

第34条 用enum代替int常量

枚举类型:由一组固定的常量组成合法值的类型

java的枚举类型是功能齐全的类,本质还是int值, 通过公有的静态final域为每个枚举常量导出一个实例。枚举类型没有可以访问的构造器,是真正的final类,是实例受控的。

枚举类型允许添加任意的方法和域,并实现任意的接口(提供了所有的Object方法的高级实现,实现了Comparable、Serializable接口…)

为了将数据与枚举常量联系起来,需要声明实例域(均为final),并编写构造器

public enum Demo {
    A(1, 2),
    B(3, 4),
    C(1, 1);
    
    private final int i1;
    private final int i2;
    
    Demo(int i1, int i2){
        this.i1 = i1;
        this.i2 = i2;
    }
    
    public int sum(){
        return i1 + i2;
    }
}

Enum类的常用方法

方法描述
values()以数组的形式返回枚举类型的所有成员
valueOf()将普通字符串转换为枚举实例
compareTo()比较两个枚举成员在定义时的顺序
ordinal()获取枚举成员的索引位置
toString()返回每个枚举值的声明名称

如果一个枚举具有普遍适应性,则应该成为顶层类, eg: java.math.RoundingMode(十进制小数舍入模式)

特定与常量的方法实现

public enum 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;
    Operation(String symbol){
        this.symbol = symbol;
    }
    public abstract double apply(double x, double y); // 必须是抽象的,每个枚举实例都要实现  
}

策略枚举

public enum PayrollDay{
    MONAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
    SATURDAY(PayType.WEEKEND), SUNDAY(Paytype.WEEKEND);
    
    private final PayType payType;
    
    payrollDay(PayType payType) { 
        this.payType = payType;
    }
    PayrollDay(){this(PayType.WEEKDAY)}
    
    private enum PayType{ // 私有的嵌套枚举类
        WEEKDAY{
            int overtimePay(int mins, int payRate){
                return ...
            }
        },
        WEEKEND{
            int overtimePay(int mins, int payRate){
                return ...
            }
        };
        abstract int overtimePay(int mins, int payRate);
        private static final int MINS_PER_SHIFT = 8 * 60;
        
        int pay(int mins, int payRate){
            int basePay = mins * payRate;
            return basePay + overtimePay(mins, payRate);
        }
    }
}
  • 每当需要一组固定的常量,并且在编译时就知道其成员的时候,就应该使用枚举
  • 每个枚举常量都需要不同的行为,考虑特定于常量的方法
  • 多个但非所有枚举常量同时共享相同的行为需要考虑策略枚举

第35条 用实例域代替序数

永远不要根据实例的序数导出与它关联的值

// 错误示例  // 问题在于如果修改枚举常量增加或减少,则ordinal()会变,可能造成错误使用
public enum NoDoThis{
    A, B, C;
    public int getOrder() { return ordinal() + 1;}
}
// 正确示例
public enum DoThis{
    A(1), B(11), C(333);
    
    private final int order;
    DoThis(int order) { this.order = order};
    public int getOrder() {return order;}
}

一般不使用ordinal(), 它是设计用于像 EnumSet 和 EnumMap 这种基于枚举的通用数据结构


第36条 用EnumSet代替位域

用 OR 位运算符将几个常量合并到一个集合中,称作位域

// 位域
public class Text{
	public static final int STYLE_BOLD = 1 << 0;
    public static final int STYLE_ITALIC = 1 << 1;
    ...
    public void applyStyles(int styles){...}
}
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

// EnumSet
public class Text{
    public enum Style{
        BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
    }
    
    public void applyStyles(Set<Style> styles){...}
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
  • EnumSet 如果底层的枚举类型少于等于64个,则用long表示
  • applyStyles的参数类型用的是Set,扩展性更好

常用方法

allOf(Class elementtype)创建一个包含指定枚举类型中所有枚举成员的EnumSet对象
complementOf(EnumSet s)创建一个与指定EnumSet对象s相同的枚举类型EnumSet对象,并包含所有s中未包含的枚举成员
copyOf(EnumSet s)创建一个与指定EnumSet对象s相同的枚举类型EnumSet对象,并与s包含相同的枚举成员
noneOf(Class elementType)创建空的EnumSet对象
of(E first, E… rest)创建包含指定枚举成员的EnumSet对象
range(E from, E to)创建包含了from到to之间的枚举成员的EnumSet对象
for(Style st : EnumSet.range(Style.BOLD, Style.UNDERLINE)){
    dosomething(st);
}

第37条 用EnumMap 代替序数索引

class Plant{
    enum LifeCycle{
        A, B, C
    }
    final String name;
    final LifeCycle lc;
    
    Plant(String name, LifeCycle lc){
        this.name = name;
        this.lc = lc;
    }
    @Override public String toString(){
        return name;
    }
}
// 按照 LifeCycle分组
Set<Plant>[] plc = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plc.length; i++)
    plc[i] = new HashSet<>();
for(Plant p : garden)
    plc[p.LifeCycle.ordinal()].add(p);
// 泛型数组问题很多,且不建议使用ordinal()

// 使用EnumMap优化 (推荐) 
Map<Plant.LifeCycle, Set<Plant>> plc = new EnumMap<>(Plant.LifeCycle.class);
for(Plant.LifeCycle lc : Plant.LifeCycle.values)
    plc.put(lc, new HashSet<>());
for(Plant p : garden)
    plc.get(p.lc).add(p);

// 使用流继续优化
Arrays.stream(garden).collect(groupingby(p -> p.lifeCycle))
// 问题在于选择了自己的映射实现,并非EnumMap,继续优化,使用三参数的 groupingby
Arrays.stream(garden).collect(groupingby(p->p.lifeCycle, 
                                         ()->new EnumMap<>(LifeCycle.class), toSet()))

如果表示的关系是多维的,使用嵌套的EnumMap

public enum Phase{
    SOLID, LIQUID, GAS;
    public enum Transition{
        MELT(SOLID, LIQUID), SUBLIME(SOLID, GAS),
        FREEXE(LIQUID, SOLID), BOIL(LIQUID, GAS),
        CONDENSE(GAS, LIQUID), DEPOSIT(GAS, 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(groupingby(
        		t -> t.from,   // key 
        		() -> new EnumMap<>(Phase.class), // 修改返回值的类型
        		toMap(t->to,  // value // key
                      t->t,   // vlaue
                      (x,y)->x, // merge 
                      ()->new EnumMap<>(Phase.class)))); // 返回值类型
    }
}

最好不要用序数来索引数组,使用EnumMap


第38条 用接口模拟可扩展的枚举

目前没有很好的方法来枚举基本类型的所有元素及其扩展,枚举的可伸缩性会导致设计及实现复杂

虽然枚举类型是不可扩展的,但是接口是可以扩展的

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; }
    }
    ...
    private final String symbol;
    BasicOperation(String symbol){
        this.symbol = symbol;
    }
}
// 扩展 不必像在不可扩展的枚举类中利用特定的实例方法来声明抽象的apply方法
public enum ExtendedOperation implements Operation{
    PLUS("^"){
        public double apply(double x, double y){ return Math.pow(x, y); }
    }
    ...
    private final String symbol;
    BasicOperation(String symbol){
        this.symbol = symbol;
    }
}

// 方法一 类的字面文件充当有限制的类型令牌
private static <T extends Enum<T> & Operation>> void test(class<T> opEnumType, double x, double y){
    for(Operation op: opEnumType.getEnumConstants()){
        sout("%f", op.apply(x, y));
    }
}

// 方法二 有限制的通配符类型
private static void test1(Collection<? extends Operation> opSet, double x, double y){
    for(Operation op: opSet){
         sout("%f", op.apply(x, y));
    }
}

public static void main(String args[]){
    double x = 1.1;
    double y = 2.2;
    test(ExtendedOperation.class, x, y);
    test1(Arrays.asList(ExtendedOperation.values()), double x, double y);
}

虽然无法编写可扩展的枚举类型,但可以通过编写接口以及实现该接口的基础枚举类型来模拟可扩展的枚举类型


第39条 注解优于命名模式

命名模式:表明有些程序元素需要通过某种工具或框架进行特殊处理

缺点:1、要求一定的方法名格式,例如Junit之前要求方法以test开头,易写错

2、无法确保只用在相应的程序元素上

3、 没有提供将参数值与程序元素关联起来的好办法

不带参的注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test{
}

public class Sample{
    @Test public static void m1(){}
}

public class RunTests{
    public static void main(String args[]){
        int test = 0;
        int pass = 0;
        Class<?> testClass = Class.forName("Sample");     // 反射获取methods
        for(Method m : testClass.getDeclaredMethods(0)){
            if(m.isAnnorationPersent(Test.class)){
                test++;
                try{
                    m.invoke(null);
                    pass++;
                }catch(InvocationTargetException ex){
                    Throwable exc = ex.getCause();
                    ...
                }catch(Exception e){
                    ...
                } 
            }
        }
    }
}

带参注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
	Class<? extends Exception> value();
}

public class Sample2{
    @ExceptionTest(ArithmeticException.class)
    public static void m1(){
        int i = 0;
        i = i / 1;
    }
}

if(m.isAnnorationPersent(ExceptionTest.class)){
    test++;
    try{
        m.invoke(null);
        pass++;
    }catch(InvocationTargetException ex){
        Throwable exc = ex.getCause();
        // 提取注解参数的值,检验是否为正确的类型
        Class<? extends Exception> excType = m.getAnnoatation(ExceptionText.class).value();
        if(excType.isInstance(exc)){
            passed++;
        }else{
            ...
        }
        ...
    }catch(Exception e){
        ...
    } 
}

注解的参数类型改为Class对象的数组

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
	Class<? extends Exception>[] values();
}

public class Sample3{
    // 花括号将元素包裹,并用逗号分开
    @ExceptionTest({IndexOutOfBoundsException.class, NullPointerException.class})
	public static void doubleBad(){
        List<String> list = new ArrayList<>();
        list.addAll(5, null);
    }
}

if(m.isAnnorationPersent(ExceptionTest.class)){
    test++;
    try{
        m.invoke(null);
        pass++;
    }catch(Throwable ex){
        Throwable exc = ex.getCause();
        // 提取注解参数的值,检验是否为正确的类型
        Class<? extends Exception>[] excTypes = m.getAnnoatation(ExceptionText.class).value();
        for( Class<? extends Exception> et: excTypes){
            if(et.isInstance(exc)){
            	passed++;
                break; // 只验证一个即可
        	}else{
            	...
        	}
        }
        ...
    }
}

@Repeatable 元注解对注解的声明进行注解,只有一个参数:包含注解类型的类对象,参数为注解类型数组

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest{
    Class<? extends Exception> value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer{
	ExceptionTest[] value();
}

// use
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad(){...}

// 重复的注解会生成一个包含注解类型的合成注解,但重复的注解不是注解类型的一部分
// 必须检查注解类型及其包含的注解类型
if(m.isAnnorationPersent(ExceptionTest.class) || m.isAnnorationPersent(ExceptionTestContainer.class)){
    test++;
    try{
        m.invoke(null);
        pass++;
    }catch(Throwable ex){
        Throwable exc = ex.getCause();
        ExceptionTest[] excTypes = m.getAnnoatationByType(ExceptionText.class).value();
        for(ExceptionTest et: excTypes){
            if(et.isInstance(exc)){
            	passed++;
                break; // 只验证一个即可
        	}else{
            	...
        	}
        }
        ...
    }
}
// 逻辑上是将同一个注解类型的多个实例应用到一个指定的程序元素

第40条 坚持使用Override注解

@Override只能使用在方法声明中,表示被注解的方法覆盖了超类中的方法声明,坚持使用防止很多错误

public class DelDemo {
    private char first;
    private char second;

    DelDemo(char first, char second){
        this.first = first;
        this.second = second;
    }

    public boolean equals(DelDemo d){
        return d.first == first && d.second == second;
    }

    public int hashCode() {
        return 31 * first + second;
    }

    public static void main(String[] args) {
        Set<DelDemo> s = new HashSet<>();
        for(int i = 0 ; i < 10; i++){
            for(char ch = 'a'; ch <='z'; ch++){
                s.add(new DelDemo(ch, ch));
            }
        }
        System.out.println(s.size());
    }
}

程序输出 260 ,因为set中add未使用DelDemo中的equals方法判断是否相等,所以每new一次都是新的对象,如果加上@Override,则结果为26 , add不能加入重复元素

    @Override
    public boolean equals(Object dd){
        DelDemo d = (DelDemo)dd;
        return d.first == first && d.second == second;
    }
    @Override
    public int hashCode() {
        return 31 * first + second;
    }

覆盖超类的方法声明都应该加上@Override


第41条 用标记接口定义类型

标记接口:不包含方法声明的接口

标记接口对于标记注解的优势

  • 标记接口定义的类型由被标记的类的实例实现,标记注解没有定义这样的类型
  • 允许在编译时捕捉到注解运行时才能捕捉到的错误
  • 可以被更加精确地锁定
  • 标记注解是更大注解的一部分,在支持注解作为编程元素的框架中具有一致性

eg:

java.io.Serializable 表明实现该接口才能序列化,反序列化

java.lang.Cloneable, java.util.RandomAccess …

选择:

标记接口:想要定义一个任何新方法不不会与之关联的类型

标记注解:想要标记程序元素而非类和接口


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值