《Effective Java》读书笔记 - 6.枚举和注解

Chapter 6 Enums and Annotations

Item 30: Use enums instead of int constants

Enum类型无非也是个普通的class,所以你可以给他加class能有的东西,比如constructor:

public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS (4.869e+24, 6.052e6),
    EARTH (5.975e+24, 6.378e6);
    private final double mass; // In kilograms
    private final double radius; // In meters
    // Constructor,不能是public的哦(我猜的)
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
}

我记得我在thinking in java里看过,比如这里的EARTH无非就是编译器自动生成的Plannet类型的一个field:static final Plannet EARTH = Planet(5.975e+24, 6.378e6)。
那么如果你想加一些constant specific(不同的enum实例有不同的行为)的行为的话,可能你会用这么做:

public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
    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);
    }
}

这里我学到一招,如果不写那句throw,编译会不通过“你必须在每个代码可能到达的地方写一个return”。但是,我个人感觉,switch case是一切不良编程习惯的典型代表,如果你要加一个新的enum constant,你还要加一个case,很可能你就忘了然后就吃瘪了。于是你可以用下面这种语法:

// Enum type with constant-specific method implementations
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;} };

    abstract double apply(double x, double y);
}

应该很好懂所以我不解释了。
每一个Enum类型都有一个valueOf(String),从一个constant的名字得到一个真正的这个constant,但是如果你override了toString方法,也就意味着你的constant的名字和toString不一致,所以最好写一个fromString(String)方法,方法参考书上。
但是上面这种解决constant specific的方法有个缺点就是,不能“share code”,比如

// Enum that switches on its value to share code - questionable
enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,SATURDAY, SUNDAY;
    private static final int HOURS_PER_SHIFT = 8;
    double pay(double hoursWorked, double payRate) {
        double basePay = hoursWorked * payRate;
        double overtimePay; // Calculate overtime pay
        switch(this) {
            case SATURDAY: case SUNDAY:
                overtimePay = hoursWorked * payRate * 2;//假如周末是两倍加班费
            default: // Weekdays
                overtimePay = hoursWorked <= HOURS_PER_SHIFT ?0 : (hoursWorked -  HOURS_PER_SHIFT) * payRate * 1.5 ;//假如平时是1.5倍
                break;
        }
        return basePay + overtimePay;
    }
}

这里的pay方法就是根据输入的某一天的工作小时数和payrate(就是你每小时的基本工资)计算出这一天的总薪酬(基本工资+加班费),比如计算星期一的薪酬:MONDAY.pay(10, 50)。
刚才说过了,switch case这种写法没有很好的可维护性,刚才介绍的“constant-specific method”语法也一样,你只能通过增加一些helper method来减少重复代码,不管怎么样都会降低可读性。
其实你可以想一下这个问题的本质:对于每个上面enum constant,你都需要一个计算overtime pay的strategy,所以你可以把这件事委托给另一个人专门负责(Strategy Pattern):

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);//委托给payType去做
    }
    // The strategy enum type
    private enum PayType {
        WEEKDAY {
            double overtimePay(double hours, double payRate) {
                return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
                        * payRate * 1.5;
            }
        },
        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);
        }
    }
}

虽然这个方法乍一看很繁琐,但是如果你的enum constants一旦变多就会体现出优势(所有strategy pattern的优势)。这个pattern适用于这种情况:if multiple enum constants share common behaviors。
虽然switch不适用于enum的内部实现,但是对某个enum的client来说还是很适用的。

Item 31: Use instance fields instead of ordinals

每一个enum constant都关联着一个int,而你可以用ordinal()来获得这个int。这条item的意思就是,永远都不要依靠ordinal()来计算或得到某个状态值,甚至最好完全不要用ordinal(),因为这个方法主要是被设计成服务于EnumSet和EnumMap的,举例:假设你要给每一个enum constant编个号,千万别这样:

// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Student {
    JACK,JOE,JANE,JAKE;
    public int studentNumber() { return ordinal() + 1; }
}

坏处一大堆,比如如果你声明enum constants的顺序变了,那么各个学生的编号就变了。正确做法是:

public enum Student {
    JACK(1),JOE(2),JANE(3),JAKE(4);
    private final int studentNumber;
    Student(int number) { this.studentNumber= number; }
    public int studentNumber() { return studentNumber; }
}

Item 32: Use EnumSet instead of bit fields

以前看Win32编程的时候经常看到这种:

// Bit field enumeration constants - OBSOLETE!
public class Text {
    public static final int STYLE_BOLD = 1 << 0; // 1
    public static final int STYLE_ITALIC = 1 << 1; // 2
    public static final int STYLE_UNDERLINE = 1 << 2; // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
    // Parameter is bitwise OR of zero or more STYLE_ constants
    public void applyStyles(int styles) { ... }
}

然后你可以这么用:text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
现在有更好的选择:EnumSet,它实现了Set接口,于是可以把上面的改进一下:

// EnumSet - a modern replacement for bit fields
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) { ... }
}

然后可以这么用:text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
唯一的缺点是1.6版本的EnumSet不是immutable的。

Item 33: Use EnumMap instead of ordinal indexing

EnumMap是“key是Enum类型的Map”,比HashMap更高效,其内部用数组实现。
当你需要把一种enum constant映射到另一个value的时候,不要用”用ordinal方法作为数组下标“的数组 的方法,可读性和可维护性都很差。举个具体的例子吧(复习时可以选择性地跳过),首先千万别这样:

//植物类,分为“一年绿一次”,“常年绿”,“半年绿一次”三种类型。
public class Herb {
    public enum Type { ANNUAL, PERENNIAL, BIENNIAL }
    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;}
}

然后现在我们现在有a list of herbs,然后要根据类型分类,把相同类型的herb放到一个Set里去,所以有三种类型就有三个Sets,我们用一个Set[]数组herbsByType来表示:

// Using ordinal() to index an array - DON'T DO THIS!
Herb[] garden = ... ;
// Indexed by Herb.Type.ordinal()
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);
// Print the results,也就是怎么取回来
for (int i = 0; i < herbsByType.length; i++) {
    System.out.printf("%s: %s%n",Herb.Type.values()[i], herbsByType[i]);
}

这种做法问题一大堆,随便举一个:因为数组和泛型不兼容,所以你必须有一个有警告的cast:(Set<Herb>[])(这里我暂停想了一下为什么不能创建一个Set<Herb>的数组,个人觉得是因为数组是runtime保证type safe的,到了runtime的时候Set都等于Set,数组分不清)。正确做法是:

// Using an EnumMap to associate data with an enum
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的constructor的参数是一个Type Token:Herb.Type.class。

有的时候你可能需要两个enum作为key,就相像你需要一个二维平面的坐标(x,y)来得到一个位置。书上举得例子是:从liquid到solid是freezing,从liquid到gas是boiling等等,也就是:从两个“物质形态enum“(固态液态气态)map到一种”变化enum“(freezing,boiling...)。(艹,我顺便去复习了一下初中物理,原来从固态到气态叫升华(SUBLIME)) 下面举一下反例:

// Using ordinal() to index array of arrays - DON'T DO THIS!
public enum Phase { SOLID, LIQUID, GAS;
    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
        // Rows indexed by src-ordinal, cols by dst-ordinal
        private static final Transition[][] TRANSITIONS = {
            { null, MELT, SUBLIME },
            { FREEZE, null, BOIL },
            { DEPOSIT, CONDENSE, null }
         };
         // Returns the phase transition from one phase to another
         public static Transition from(Phase src, Phase dst) {
             return TRANSITIONS[src.ordinal()][dst.ordinal()];
         }
    }
}

这个二维数组想象成一张表格就行。那么用EnumMap怎么做?这里我觉得需要一种“技巧”,因为EnumMap的key没法定义成“(x,y)”这种形式,所以你可以这样声明它的类型:Map<Phase, Map<Phase,Transition>> m,可以这么理解:第一次指定一个Phase的时候:m.get(src)得到的是“那张表格的某一行”,然后接着第二次get的时候:m.get(src).get(dst)得到的就是刚才那一行的某一列了。我个人认为,这种“技巧”是为了得到EnumMap的性能,如果不需要考虑性能,完全可以用"(x,y)"作为一个HashMap的key,感觉会比这个“技巧”好理解,从而提升可读性。下面放一下具体的代码:

public enum Phase {
    SOLID, LIQUID, GAS;
    public enum Transition {
         MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), BOIL(LIQUID, GAS), CONDENSE(
                GAS, LIQUID), SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
        private final Phase src;
        private final Phase dst;
        Transition(Phase src, Phase dst) {
            this.src = src;
            this.dst = dst;
        }
        // Initialize the phase transition map
        private static final Map<Phase, Map<Phase, Transition>> m = new EnumMap<Phase,    
                        Map<Phase, Transition>>(Phase.class);//传入作为key的Enum类型
        static {
            for (Phase p : Phase.values())
                m.put(p, new EnumMap<Phase, Transition>(Phase.class));
            for (Transition trans : Transition.values())
                m.get(trans.src).put(trans.dst, trans);//这句加粗
        }

        public static Transition from(Phase src, Phase dst) {
            return m.get(src).get(dst);
        }
    }
}

你可以注意到这里把Transition这个Enum Type扩展了一下,为的是上面那句加粗的语句,同时也提供了“(x,y)到z的映射”,这样就不用再被“下标是几的元素对应的是谁”的问题搞脑子了,此外,如果你要新加一种enum constant的话,这种写法甚至不需要你修改任何“EnumMap的部分”,很爽。

Item 34: Emulate extensible enums with interfaces

因为Enum类型不能被继承,如果你想“模拟继承”,可以给你的Enum加个接口。而你的Enum的所有API都是通过这个接口暴露出来的,也就是client在用你的Enum的时候,变量类型肯定是这个接口。这样的话client就可以实现自己的Enum类型,只要实现这个接口就行,然后任何需要“base enum”的地方,你都可以用你的“extension enum”来代替。个人理解:这里说继承不如说是“扩展”,只是如果库的enum类型无法满足你的需求,你就可以实现自己的,但是你自己的并没有包含原先enum类型中的任何功能。
作为类库实现者,你可以定义如下的方法,让其参数可以接收client自定义的enum:

public static <T extends Enum<T> & Operation> void test(Class<T> opSet)

<T extends Enum<T> & Operation>的意思是T必须满足 是一个Enum类型并且实现了Operation这个接口。然后比如client实现了一个叫ExtendedOperation的enum类型,直接传ExtendedOperation.class进去就行了。
或者你也可以把方法定义成:

public static void test(Collection<? extends Operation> opSet)

这样的好处是client可以自己先combine一些opSet(来自“base enum”的也好,来自其“extension enum”的也好)到同一个集合,然后再传进去,比如用Arrays.asList(ExtendedOperation.values())
但缺点是在方法内无法用EnumSet和EnumMap。

Item 35: Prefer annotations to naming patterns

这里的naming patterns就是指在Annotation出现之前,只能用某种约定的命名格式来完成 现在用annotation可以强制 的事。比如JUnit起初要求测试方法名必须以test开头,这种做法的坏处一大堆。而用annotation就好很多,下面举几个Annotation定义和用法的具体例子(复习时可以选择性跳过):

/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

上面那段注释是因为我们在tool或framework中用 被这个Annotation mark过的方法 的时候,假定都是static和parameterless的方法, 往下看你就懂了。用这个Annotation很简单,在方法声明上面一贴就行,而这么一贴对方法本身或者其所在的类没什么影响,只是一个信息(提供给那些对这个信息感兴趣的tools或者framework),比如像这样:

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);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

因为上面用了m.invoke(null),所以如果没有遵守“必须在static和parameterless的方法上mark这个Annotation“的规定,就会抛出InvocationTargetException。
现在我们再写一个Annotation,用来标记那些“期待指定异常”的方法:

// Annotation type with a parameter
/**
* Indicates that the annotated method is a test method that
* must throw the designated exception to succeed.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception> value();
}

我发现定义Annotation的时候不需要指定构造函数,只需要指定它的fields就行了。下面是这个Annotation的用法:

@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
    int i = 0;
    i = i / i;
}

下面是相应的“Test Runner”的代码:

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(ExceptionTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception%n", m);
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    Class<? extends Exception> excType =
                                                m.getAnnotation(ExceptionTest.class).value();
                    if (excType.isInstance(exc)) {
                                                passed++;
                                        } else {
                                                System.out.printf("Test %s failed: expected %s, got %s%n",
                                                        m, excType.getName(), exc);
                                        }
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

这段代码从我们定义的ExceptionTest中提取出了value字段,然后判断和真正抛出的异常是不是一致。
再要加强的话,你还可以修改ExceptionTest让它接受一系列异常的class object,表示抛出其中任何一个异常都视为test通过,那么你只要把value这个字段改一下就行:

Class<? extends Exception>[] value();//只是变成数组而已

然后在用这个Annotation的时候,刚才上面所有的“single parameter”语法也是可以的,表示一个“single-element array”。而“multi-element”的参数语法如下:

@ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class })
public static void someMethod(){...}

对应的test runner也很好改,这里就不写了。
所以说,其实除了tool或者某些framework的实现者,我们一般是不需要定义Annotation的,但是我们应该用 tool或者framework以及Java platform本身提供的Annotation。

Item 36: Consistently use the Override annotation

像我就曾经因为C#带过来的命名习惯,把toString写成ToString结果也没加@Override,结果就变成overload了。所以应该养成用@Override的习惯,但是override接口或抽象类中的方法时,可以不加@Override

Item 37: Use marker interfaces to define types

一个marker interface就是一个里面啥方法声明都没有的interface。一个类实现这么一个interface只是为了“标记”一下自己有某种特点,比如Serializable这个interface。与这个marker interface类似的就是“marker annotation”,比如上上个item中最一开始的那个Test就是。作者认为marker interface和marker annotation各有各的好和坏。看到这我不经想问“你TM不是在讲clone方法的时候说用interface来标记某种特点是不对的吗?”,然后我貌似理解他的意思了:作者的意思是:Object里面的clone方法不应该接受一个Object类型的参数然后再if(obj instanceof Cloneable),而应该直接让Object里面的clone方法接受一个Cloneable类型的参数,这样就直接在编译时保证类型安全而不至于到运行时出错。marker annotation的优点在于可以“不断进化和改进”,而且可以贴在方法上;而marker interface一旦定义后就定死了,以后就不能再往里面新增方法了(Item 18)。

转载于:https://www.cnblogs.com/raytheweak/p/7202466.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值