Effective Java 读书笔记(五):枚举和注解

  • Effective Java 读书笔记五枚举和注解
    • 用 enum 代替 int 常量
    • 用实例域代替序数
    • 用 EnumSet 代替位域
    • 使用 EnumMap 代替序数索引
    • 用接口模拟可伸缩的枚举
    • 注解优先于命名模式
    • 坚持使用 Override 注解
    • 用标记接口定义类型

Effective Java 读书笔记(五):枚举和注解

用 enum 代替 int 常量

枚举类型 enum type 是指由一组固定常量组成合法值的类型,比如一年中的季节、太阳系中的行星。

在编程语言中还没有枚举类型之前,表示枚举类型的常用模式是声明一组 int 常量,每个类型成员一个常量:

    public static final int APPLE_FUJI = 0;
    public static final int APPLE_PIPPIN = 1;
    public static final int APPLE_GRANNY = 2;

    public static final int ORANGE_NAVEL = 0;
    public static final int ORANGE_TEMPLE = 1;
    public static final int ORANGE_BLOOD = 2;

这种模式被称为 int 枚举模式,有诸多不足:
1. 类型安全性:以上的 APPLE 和 ORANGE 本来是不同类型,然而都用 int 表示,那么如果将 APPLE 传到了需要 ORANGE 的地方,编译器也不会发现问题。
2. 由于没有命名空间,每个枚举值都需要用前缀 APPLE_ 或 ORANGE_ 来区分。
3. 将 int 枚举常量翻译成可打印的字符串,并没有很便利的方法。
4. int 枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的 int 值发生了变化,那么客户端也需要重新编译。

从 Java1.5 开始,出现一个特殊的枚举类型:

public enum Apple {
    FUJI,
    PIPPIN,
    GRANNY;
}

public enum Orange {
    NAVEL,
    TEMPLE,
    BLOOD;
}

枚举类型 enum 是实例受控的,是单例的泛型化,同时也是类型安全的。同时,其默认的 toString 方法输出的是枚举常量名字符串,比如 FUJI、PIPPIN。

Java 中的枚举本质上是 int 值,可以使用 ordinal() 获取,默认从 0 开始依次递增。

调整枚举常量的顺序后,并不需要重新编译客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中。比如下面的这个类,被反编译后我们发现字节码中存储的是枚举类型和常量名,而不是 int 值:

public class TestEnum2 {
    public static void main(String[] args) {
        System.out.println(Apple.FUJI);
    }
}
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Fieldref           #23.#24        // com/qunar/flight/tts/afare/utils/Apple.FUJI:Lcom/qunar/flight/tts/afare/utils/Apple;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: getstatic     #3                  // Field com/qunar/flight/tts/afare/utils/Apple.FUJI:Lcom/qunar/flight/tts/afare/utils/Apple;
         6: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
         9: return

Java 中的枚举 enum 也可以拥有 field、方法、抽象方法,还可以覆盖 toString 方法:

public enum Operation {
    PLUS("+") {
        @Override
        double apply(double a, double b) {
            return a + b;
        }
    },
    MINUS("-") {
        @Override
        double apply(double a, double b) {
            return a - b;
        }
    },
    TIMES("*") {
        @Override
        double apply(double a, double b) {
            return a + b;
        }
    },
    DIVIDE("/") {
        @Override
        double apply(double a, double b) {
            return a + b;
        }
    };

    private final String symbol;
    Operation(String symbol) {
        this.symbol = symbol;
    }

    // 覆盖 toString,输出 symbol
    @Override
    public String toString() {
        return symbol;
    }

    // 将 symbol 转换成 Operation
    public Operation fromString(String symbol) {
        for (Operation operation : values()) {
            if (operation.symbol.equals(symbol)) {
                return operation;
            }
        }
        return null;
    }

    abstract double apply(double a, double b);
}

考虑用一个枚举表示薪资包中的工作天数,这个枚举有一个方法,根据给定工人的基本工资以及当天工作时间,计算当天报酬。在 5 个工作日中,超过 8 小时会产生加班工资,而在周末则所有工作都产生加班工资。解决这一问题的比较好的方案是使用策略枚举模式:将加班工资的计算移到了私有的嵌套枚举中,将某一天适用的策略枚举传递到 PayrollDay 的构造器中。PayrollDay 中不再需要 switch 语句或特定于常量的方法实现,这种模式虽然没有 switch 语句简洁,但是更加安全(减少增加枚举时遗漏增加逻辑的可能性),也更加灵活。

public 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);
    }

    private enum PayType {
        WEEKDAY {
            @Override
            double overtimePay(double hrs, double payRate) {
                return hrs <= HOURS_PER_SHIFT ? 0 : (hrs - HOURS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            @Override
            double overtimePay(double hrs, double payRate) {
                return hrs * 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);
        }
    }
}

用实例域代替序数

所以枚举都有一个 ordinal 方法,返回每个枚举常量在类型中的数字位置,其值与枚举常量的顺序相关,完全由编译器定义。

ordinal 一般是用于像 EnumSet、EnumMap 这样的通用数据结构的。在程序中,不要去调用 ordinal 方法,如果需要这样的一个 int 值,请在实例域中维护。

用 EnumSet 代替位域

位域表示法允许利用位操作,有效地执行像 union 并集、intersection 交集这样的集合操作。位域有着 int 枚举常量所有的缺点,可以使用 EnumSet 代替,EnumSet 就是用单个 long 表示,很多操作都是利用位算法来实现,其性能和位域相当。

    public static final int STYLE_BOLD = 1 << 0;
    public static final int STYLE_ITALIC = 1 << 1;
    public static final int STYLE_UNDERLINE = 1 << 2;

    // 使用例子
    int STYLE_BOLD_ITALIC = STYLE_BOLD | STYLE_ITALIC;
// RegularEnumSet 是 EnumSet 的一个实现
class RegularEnumSet> extends EnumSet {
    /**
     * Bit vector representation of this set.  The 2^k bit indicates the
     * presence of universe[k] in this set.
     */
    private long elements = 0L;

    // add 利用位操作实现
    public boolean add(E e) {
        typeCheck(e);

        long oldElements = elements;
        elements |= (1L << ((Enum)e).ordinal());
        return elements != oldElements;
    }
}

使用 EnumMap 代替序数索引

最好不要用序数 ordinal 来索引数组,而要使用 EnumMap。如果你所表达的关系是多维的,就是用 EnumMap< …, EnumMap< …>>。

EnumMap 运行速度能够与序数索引的数组相媲美,因为其内部实现也是序数索引的数组。但是,使用 EnumMap 更不容易出错(类型安全,隐藏了实现细节,替你干了脏活、累活)。

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{
    private final Class keyType;
    private transient K[] keyUniverse;
    private transient Object[] vals;

    public V put(K key, V value) {
        typeCheck(key);

        int index = key.ordinal();
        Object oldValue = vals[index];
        vals[index] = maskNull(value);
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);
    }   
}   

用接口模拟可伸缩的枚举

枚举类是 final 的,不可被继承扩展的。如果想要可伸缩的枚举,可以使用接口来模拟:

public interface IOperation {
    double apply(double a, double b);
}

public enum Operation implements IOperation {
    PLUS("+") {
        @Override
        public double apply(double a, double b) {
            return a + b;
        }
    },
    MINUS("-") {
        @Override
        public double apply(double a, double b) {
            return a - b;
        }
    },
    TIMES("*") {
        @Override
        public double apply(double a, double b) {
            return a + b;
        }
    },
    DIVIDE("/") {
        @Override
        public double apply(double a, double b) {
            return a + b;
        }
    };

    private final String symbol;
    Operation(String symbol) {
        this.symbol = symbol;
    }

    // 覆盖 toString,输出 symbol
    @Override
    public String toString() {
        return symbol;
    }
}

虽然枚举类型不是可扩展的,但是接口类型是可扩展的。在使用任何基础操作的地方,都可以使用新的枚举,只要 API 是被写成采用接口类型,而非实现。

泛型 < T extends Enum< T> & IOperation> 表示 T 既是枚举,又是 IOperation 的子类型。

注解优先于命名模式

在 Java1.5 之前,经常使用命名模式 naming pattern 表明有些程序元素需要通过某种工具或框架进行特殊处理。例如,Junit 测试框架原本要求用户一定要用 test 作为测试方法名称的开头。这种方法可行,但是有很多缺点:
1. 拼写错误后难以发现。
2. 无法确保他们只用于相应的程序元素上,容易被多种框架误用。
3. 没有提供将参数值与程序元素关联起来的好方法,方法名所能携带的信息太少。

注解能够很好的解决以上的所有问题。比如 Junit 中标记测试方法的 Test 注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test {
    static class None extends Throwable {
        private static final long serialVersionUID= 1L;     
        private None() {
        }
    }

    // 期望抛出的异常
    Class expected() default None.class;

    // 方法执行超时时间,0 表示不设置超时时间
    long timeout() default 0L; 
}

注解只是提供信息给程序使用,并不会改变被注解代码的语义。

坚持使用 Override 注解

Override 注解只能用在方法声明中,表示被注解的方法覆盖率超类中的一个方法声明。坚持使用 Override 注解能够让编译器帮你检查大量的错误。

用标记接口定义类型

标记接口 marker interface 是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口,比如 Serializable 接口。

注解也是一种标记,那么它和标记接口有什么区别呢?标记注解只提供说明信息,无法定义类型;而标记接口是一种类型,由被标记类的实例实现,能够在编译时捕捉使用注解在运行时才能捕捉到的一些错误。另一方面,标记接口可以继承别的接口,而注解不能。

标记注解相对于标记接口的一大优点是,可以给已被使用的注解类型添加更多的信息,可以被用到类、方法、域上,适用范围更广。

简单来说,他们的区别就是注解和接口的区别。

你可能感兴趣的:(Java)