带你快速看完9.8分神作《Effective Java》—— 枚举 & 注解篇(一)

简介: 34 用enum代替int常量35 用实际属性代替序数36 使用 EnumSet 替代位属性37 使用EnumMap 替代序数索引38 用接口实现可继承的枚举39 注解优先于命名模式40 坚持使用Override注解41 用标记接口定义类型

34 用enum代替int常量

每当需要一组固定常量,并且在编译时就知道常量分别都是什么时,就要用枚举。


在枚举出现之前,大家都是用int常量来表示枚举类型:


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


上述做法有很多不足:

1. 不安全且没有任何描述性

如果将apple传入了想要orange的方法里,不会报错,还会用==运算符比较 Apple 与 Orange


2. 使用int 枚举的程序很脆弱

因为 int 枚举是编译时常量,所以它们的int 值被编译到使用它们的客户端中,如果与int 枚举关联的值发生更改,则必须重新编译其客户端


3. 很难将 int 枚举常量转换为可打印的字符串

就算将其打印出来了,所看到的也只是一个数字,没什么意义


4. 没有可靠的方法来遍历所有 int 枚举常量



除了int枚举模式之外,还有String枚举模式(String enum pattern),它同样也有很多缺点:


1. 导致初级用户将字符串常量硬编码到客户端代码中,就是常说的魔法值


68.png


2. 会依赖字符串的比较操作故有很大的性能问题


Java提供的枚举类型可以很好的解决上面的问题:


public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }


Java的枚举本质上是int值,思想非常简单:通过public static final属性为每个枚举常量导出一个实例。由于没有可访问的构造方法,枚举类型实际上是 final 的。客户既不能创建枚举类型的实例也不能继承它,除了声明的枚举常量外,不能有任何实例。



有以下几条优点:

1. 保证了编译时的类型安全

如果声明参数的类型为Apple,它就能保证传到该参数上的任何非空的对象引用一定是FUJI,PIPPIN,GRANNY_SMITH之一


2. 具有相同名称常量的多个枚举类型可以共存

因为每个类都有其自己的名称空间


3. 枚举类型还允许添加任意方法和属性并实现任意接口

提供了所有 Object 方法,实现了Comparable 和 Serializable 接口



比如太阳系的八颗行星,每个行星都有质量和半径,从这两个属性可以计算出它的表面重力


每个枚举常量之后的括号中的数字是传递给其构造方法的参数


package com.wjw.effectivejava1;
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; // In kilograms
    private final double radius; // In meters
    private final double surfaceGravity; // In m / s^2
    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;
    // Constructor
    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; // F = ma
    }
}

可以根据物体在地球上的重量,打印出该物体在所有8颗行星上的重量:


public class WeightTable {
    public static void main(String[] args) {
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for (Planet p : Planet.values())
            System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
    }
}

运行结果:


69.png


如果一个枚举具有普适性,他就应该成为一个顶层类,如果他只是被用在一个特定的顶层类里,他就应该成为该顶级类的

成员类,例如java.math.RoundingMode表示小数部分的舍入模式。BigDecimal类使用了这些舍入模式,但他们却不属于BigDecimal类的一个抽象。让RoundingMode成为一个顶层类,以鼓励让任何需要舍入模式的程序员重用。



有时我们需要更多的方法,加入正在编写一个枚举类,表示计算器的加减乘除操作,还需要提供一个方法来执行每个常量所表示的运算:


public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
    // Do the arithmetic operation represented by this constant
    public 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);
    }
}

这段代码能用,但不优雅,如果添加了新的枚举常量,却忘记给switch添加相应的条件就会失败。



有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体(constant-specific class body)。这种方法被称为特定于常量的方法实现(constant-specific method implementation):

public enum Operation {
    PLUS {
        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;
        }
    };
    public abstract double apply(double x, double y);
}


特定于常量的方法实现可以与特定于常量的数据结合使用。toString 方法返回与操作关联的符号:

public enum Operation {
    PLUS("+") {
        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;
    Operation(String symbol) {
        this.symbol = symbol;
    }
    @Override
    public String toString() {
        return symbol;
    }
    public abstract double apply(double x, double y);
}

上述代码可以很容易地打印算术表达式:


public static void main(String[] args) {
       double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Operation op : Operation.values())
                System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}


2.000000 + 4.000000 = 6.000000

2.000000 - 4.000000 = -2.000000

2.000000 * 4.000000 = 8.000000

2.000000 / 4.000000 = 0.500000


特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得更加困难。例如,考虑用一个枚举代表工资包中的工作天数,根据给定的某工人的基本工资(每小时)和当天工作的时间计算当天工人的工资。


enum PayrollDay {
    MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
    private static final int MINS_PER_SHIFT = 8 * 60;
    int pay(int minutesWorked, int payRate) {
        int basePay = minutesWorked * payRate;  // 计算基本工资
    // 计算加班工资
        int overtimePay;
        switch (this) {
            case SATURDAY:
            case SUNDAY: // Weekend
                overtimePay = basePay / 2;
                break;
            default: // Weekday
                overtimePay = minutesWorked <= MINS_PER_SHIFT ?
                        0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
        }
        return basePay + overtimePay;
    }
}

假设你给枚举添加了一个元素,可能是一个特殊的值来表示一个假期,但忘记在switch 语句中添加一个相应的case 条件。该程序仍然会编译,但付费方法会将节假日的工资算成工作日的工资,原因是走了上面default里的逻辑。



我们真正想要的是每次添加枚举常量时,就自动选择加班费策略:再定义一个嵌套枚举类PayType,并将PayType实例传递给·PayrollDay·枚举的构造方法里。


public enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
    SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
    private final PayType payType;
    PayrollDay(PayType payType) {
        this.payType = payType;
    }
    PayrollDay() {
        this(PayType.WEEKDAY);
    } // Default
    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }
    // The strategy enum type
    private enum PayType {
        WEEKDAY {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0 :
                        (minsWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked * payRate / 2;
            }
        };
        abstract int overtimePay(int mins, int payRate);
        private static final int MINS_PER_SHIFT = 8 * 60;
        int pay(int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            return basePay + overtimePay(minsWorked, payRate);
        }
    }
}


枚举的switch语句适合于给外部的枚举类型增加和常量值对应行为。


假设希望 Operation 枚举有一个实例方法来返回每个相反的操作。


public static Operation inverse(Operation op) {
  switch(op) {
     case PLUS: return Operation.MINUS;
     case MINUS: return Operation.PLUS;
     case TIMES: return Operation.DIVIDE;
     case DIVIDE: return Operation.TIMES;
     default: throw new AssertionError("Unknown op: " + op);
  }
}


35 用实际属性代替序数


许多枚举天生就与某个 int 值关联。所以枚举都有一个ordinal方法,返回每个枚举常量在类型中的数字位置。

public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET;
    public int numberOfMusicians() {
        return ordinal() + 1;
    }
}

这段代码维护起来就是一场噩梦,如果上面常量的顺序变了,所有用到numberOfMusicians的地方都会返回一个不同的值。所以永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例属性中


public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);
    private final int numberOfMusicians;
    Ensemble(int size) {
        this.numberOfMusicians = size;
    }
    public int numberOfMusicians() {
        return numberOfMusicians;
    }
}


实际上,ordinal的目的是用于基于枚举的通用数据结构,如 EnumSet 和 EnumMap,除了在编写这种数据结构时可以用,其他时候都不要用。


相关文章
|
10天前
|
Arthas Java 测试技术
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
Java字节码文件、组成、详解、分析;常用工具,jclasslib插件、阿里arthas工具;如何定位线上问题;Java注解
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
|
1天前
|
Java 数据库连接 数据格式
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
IOC/DI配置管理DruidDataSource和properties、核心容器的创建、获取bean的方式、spring注解开发、注解开发管理第三方bean、Spring整合Mybatis和Junit
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
|
4天前
|
Java 编译器 测试技术
|
15天前
|
存储 JSON 前端开发
【Java】用@JsonFormat(pattern = “yyyy-MM-dd“)注解,出生日期竟然年轻了一天
在实际项目中,使用 `@JsonFormat(pattern = &quot;yyyy-MM-dd&quot;)` 注解导致出生日期少了一天的问题,根源在于夏令时的影响。本文详细解析了夏令时的概念、`@JsonFormat` 注解的使用方法,并提供了三种解决方案:在注解中添加 `timezone = GMT+8`、修改 JVM 参数 `-Duser.timezone=GMT+08`,以及使用 `timezone = Asia/Shanghai
11 0
【Java】用@JsonFormat(pattern = “yyyy-MM-dd“)注解,出生日期竟然年轻了一天
|
24天前
|
Java
Java系列之 IDEA 为类 和 方法设置注解模板
这篇文章介绍了如何在IntelliJ IDEA中为类和方法设置注解模板,包括类模板的创建和应用,以及两种不同的方法注解模板的创建过程和实际效果展示,旨在提高代码的可读性和维护性。
|
25天前
|
Java
Java枚举使用的基本案例
这篇文章是关于Java枚举的基本使用,通过一个指令下发的代码案例,展示了如何定义枚举、使用枚举以及如何通过枚举实现指令的匹配和处理。
|
27天前
|
Java 开发者
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案。本文通过技术综述及示例代码,剖析两者在性能上的差异。if-else具有短路特性,但条件增多时JVM会优化提升性能;switch则利用跳转表机制,在处理大量固定选项时表现出色。通过实验对比可见,switch在重复case值处理上通常更快。尽管如此,选择时还需兼顾代码的可读性和维护性。理解这些细节有助于开发者编写出既高效又优雅的Java代码。
23 2
|
17天前
|
安全 Java 编译器
java枚举
java枚举
13 0
|
24天前
|
存储 缓存 Java
Java本地高性能缓存实践问题之使用@CachePut注解来更新缓存中数据的问题如何解决
Java本地高性能缓存实践问题之使用@CachePut注解来更新缓存中数据的问题如何解决
|
24天前
|
Java 编译器 开发者
【Java 第八篇章】注解
从JDK5起,Java引入注解作为元数据支持,区别于注释,注解可在编译、类加载和运行时被读取处理。注解允许开发者在不影响代码逻辑的前提下嵌入补充信息。核心概念包括`Annotation`接口、`@Target`定义适用范围如方法、字段等,`@Retention`设定生命周期,如仅存在于源码或运行时可用。Java提供了内置注解如`@Override`用于检查方法重写、`@Deprecated`标记废弃元素、`@SuppressWarnings`抑制警告。自定义注解可用于复杂场景,例如通过反射实现字段验证。
13 0