zoukankan      html  css  js  c++  java
  • 《Effective Java》读书笔记

    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)。

  • 相关阅读:
    uniapp爬坑之旅_开发一个自己的app_day27_完善数据库并给添加任务限制条件
    uniapp爬坑之旅_开发一个自己的app_day45_任务左滑不太好搞
    uniapp爬坑之旅_开发一个自己的app_day47_主页面基本完成
    uniapp爬坑之旅_开发一个自己的app_day28_完成任务条信息和数据库的同步
    uniapp爬坑之旅_开发一个自己的app_day31_增加主页面任务条滚动功能
    uniapp爬坑之旅_开发一个自己的app_day35
    uniapp爬坑之旅_开发一个自己的app_day46_实现任务左滑
    uniapp爬坑之旅_开发一个自己的app_day29_增加删除任务功能
    uniapp爬坑之旅_开发一个自己的app_day32_增加修改任务功能
    c++:class,名字空间等
  • 原文地址:https://www.cnblogs.com/raytheweak/p/7202466.html
Copyright © 2011-2022 走看看