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。标记注解是特殊类型的注解,其中不包含成员。标记注解的唯一目的就是标记声明。

  • 标记接口的优点:标记接口允许你在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误。
  • 标记注解的优点:便于扩展,可以给已被使用的注解类型添加更多信息(元注解)。而接口实现后不可能再添加方法。

标记接口与标记注解如何选择:

  • 标记接口和标记注解都各有好处。若想要定义一个任何新方法都不会与之关联的类型,就是用标记接口。若想要标记成员元素而非类和接口(方法或成员变量)或未来可能要给标记添加更多信息,就使用标记注解。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • Java 1.5发行版本新增了两个引用类型家族:枚举类型(Enumerate类)和注解类型(Annotation接...
    Timorous阅读 408评论 0 0
  • 经典重读——亚马逊链接 笔记链接 导图: 笔记文本: Effective Java1 第2章 创建和销毁对象1.1...
    8c3c932b5ffd阅读 1,424评论 0 1
  • 对象的创建与销毁 Item 1: 使用static工厂方法,而不是构造函数创建对象:仅仅是创建对象的方法,并非Fa...
    孙小磊阅读 1,981评论 0 3
  • 创建和销毁对象 静态工厂模式 构造器里未传参的成员不会被初始化。int类型是0,布尔类型是false,String...
    扬州慢_阅读 486评论 0 5
  • 每个不甘于平凡的灵魂背后想必都有一个创业的梦想,曾经的我也曾一度迷茫,直到三个月前我了解了跨境电商,这一领域所特有...
    自信的风阅读 5,383评论 0 1