Java资料整理Java学习笔记技术干货

Effective java笔记(五),枚举和注解

2016-09-15  本文已影响238人  Alent

30、用enum代替int常量

枚举类型是指由一组固定的常量组成合法值的类型。在java没有引入枚举类型前,表示枚举类型的常用方法是声明一组不同的int常量,每个类型成员一个常量,这种方法称作int枚举模式。采用int枚举模式的程序是十分脆弱的,因为int值是编译时常量,若与枚举常量关联的int发生变化,客户端就必须重新编译。

枚举类型允许添加任意的方法和域,并实现任意的接口。枚举类型默认继承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作为测试方法名称的开头。使用命名模式有几个缺点:

注解很好的解决了这些问题。关于注解的详细用法请看 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。标记注解是特殊类型的注解,其中不包含成员。标记注解的唯一目的就是标记声明。

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

上一篇 下一篇

猜你喜欢

热点阅读