《Effective Java Third》第六章总结:枚举和注解

第六章 枚举和注解

34.用枚举类型代替 int 常量

在枚举类型被添加到 JAVA 之前,表示枚举类型的一种常见模式是声明一组 int 的常量,使用public static final修饰

缺点:
没有提供任何类型安全性,也没有提供多少表达能力;
没有一种简单的方法可以将 int 枚举常量转换为可打印的字符串;
如果与 int 枚举关联的值发生了更改,则必须重新编译客户端。如果不重新编译,客户端仍然可以运行,但是他们的行为将是错误的;

使用 String 常量代替 int 常量更不可取。
虽然它确实为常量提供了可打印的字符串,但是它可能会导致不知情的用户将字符串常量硬编码到客户端代码中,而不是使用字段名。
如果这样一个硬编码的 String 常量包含一个排版错误,它将在编译时躲过检测,并在运行时导致错误。
此外,它可能会导致性能问题,因为它依赖于字符串比较。

所以最好是使用枚举来代替,因为枚举是一个类,类中再组合了此类类型的常量,能保证安全;也能通过重写toString来打印

实践1:

如下代码这个代码可以工作,但不是很漂亮。 没有 throw 语句就不能编译,因为在技术上可以访问方法的末尾,即使它永远不可能到达;
更糟糕的是,代码是脆弱的。 如果您添加了一个新的枚举常量,但是忘记为开关添加一个相应的大小写,枚举仍然会编译,但是在运行时尝试应用新操作时会失败。

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

改为如下:如果你在 Operation 枚举的第二个版本中添加一个新常量,那么你不太可能忘记提供一个 apply 方法,因为该方法紧跟每个常量声明。在不太可能忘记的情况下,编译器会提醒你,因为枚举类型中的抽象方法必须用其所有常量中的具体方法覆盖。

// Enum type with constant-specific method implementations
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);
}

实践2:

// Enum that switches on its value to share code - questionable
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 = 0;
        int overtimePay;
        switch (this) {
            case SATURDAY:
            case SUNDAY: // Weekend
                overtimePay = minutesWorked * payRate / 2;
                break;
            default: // Weekday
                basePay = minutesWorked <= MINS_PER_SHIFT ? minutesWorked * payRate : MINS_PER_SHIFT * payRate;
                overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
        }
        return basePay + overtimePay;
    }
}

这段代码非常简洁,但是从维护的角度来看,它是危险的。假设你向枚举中添加了一个元素,可能是一个表示假期的特殊值,但是忘记向 switch 语句添加相应的 case。这个程序仍然会被编译,但是 pay 方法会把假期默认当做普通工作日并支付工资。

修改为策略枚举模式:

// The strategy enum pattern
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 <= MINS_PER_SHIFT ? minsWorked * payRate : MINS_PER_SHIFT * payRate;
    		return basePay + overtimePay(minsWorked, payRate);
		}
    }
}

总结:枚举类型相对于整数常量的优势是引人注目的。
枚举更易读,更安全,更强大。
许多枚举不需要显式的构造函数或成员,但其他枚举可以从将数据与每个常量关联并提供其行为受此数据影响的方法中获益。
将多个行为与一个方法联系起来的好处更少。 在这种相对罕见的情况下,更喜欢使用特定于常量的方法,而不是使用自己的值切换的枚举。
如果某些枚举常量(但不是全部)具有共同的行为,则考虑策略枚举模式。

35.使用实例属性代替序数

永远不要从枚举的序号中得出与它相关的值; 请将其保存在实例属性中:

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方法,因为如果常量被重新排序,numberOfMusicians 方法将被破坏:

// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,SEXTET, SEPTET, OCTET, NONET, DECTET;

    public int numberOfMusicians() { return ordinal() + 1; }
}

它是为基于枚举的通用数据结构(如 EnumSet 和 EnumMap)而设计的」。除非你使用这个数据结构编写代码,否则最好完全避免使用这个方法。

36.用EnumSet 替代位字段

位字段表示方式允许使用位运算高效地执行 Set 操作,如并集和交集。但是位字段具有 int 枚举常量所有缺点,甚至更多

建议使用EnumSet 类来有效地表示从单个枚举类型中提取的值集

EnumSet 类结合了位字段的简洁性和性能和枚举类型的优点;
EnumSet 的一个真正的缺点是,从 Java 9 开始,它不能创建不可变的 EnumSet,在未来发布的版本中可能会纠正这一点。

37.使用 EnumMap 替换序数索引

序数索引:即使用ordinal方法作为数组的索引

使用EnumMap可以有效地充当从枚举到值的映射,而不用使用数组了

可以使用流来管理这个映射

如果所表示的关系是多维的,则使用 EnumMap<..., EnumMap<...>>

38.使用接口模拟可扩展的枚举

在大多数情况下,enum 的可扩展性并不是一个好主意,原因在于:
扩展类型的元素是基类的实例,而基类的实例却不是扩展类型的元素。
而且没有一种好方法可以枚举基类及其扩展的所有元素。
最后,可扩展性会使设计和实现的许多方面变得复杂。

但是,对于可扩展枚举类型,至少有一个令人信服的用例,即操作码,也称为 opcodes。
操作码是一种枚举类型,其元素表示某些机器上的操作;
有时候,我们希望 API 的用户提供自己的操作,从而有效地扩展 API 提供的操作集。

有一种很好的方法可以使用枚举类型来实现这种效果。其基本思想是利用枚举类型可以实现任意接口这一事实,为 opcode 类型定义一个接口,并为接口的标准实现定义一个枚举

使用接口来模拟可扩展枚举的一个小缺点是实现不能从一个枚举类型继承到另一个枚举类型;
如果实现代码不依赖于任何状态,则可以使用默认实现将其放置在接口中;
如果有,则可以将其封装在 helper 类或静态 helper 方法中,以消除代码重复,达到代码重用

39.注解优于命名模式

命名模式的缺点:

1.排版错误会导致没有提示的失败。

例如,假设你意外地将一个测试方法命名为 tsetSafetyOverride,而不是 testSafetyOverride。
JUnit 3 不会报错,但它也不会执行测试,这导致一种正确执行了测试的假象。

2.无法确保命名模式仅用于适当的程序元素。

例如,假设调用了一个类 TestSafetyMechanisms,希望 JUnit 3 能够自动测试它的所有方法,而不管它们的名称是什么。
同样,JUnit 3 不会报错,但它也不会执行测试。

3.没有提供将参数值与程序元素关联的好方法。

假设你希望支持只有在抛出特定异常时才成功的测试类别。
异常类型本质上是测试的一个参数。你可以使用一些精心设计的命名模式,将异常类型名称编码到测试方法名称中,但这样的代码将不好看且脆弱;
编译器将无法检查这些用于命名异常的字符串是否确实执行了。
如果指定的类不存在或不是异常,则在运行测试之前不会被发现。

注解就解决了这三个问题,如@Test注解,其原理如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {

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

@ExceptionTest:只在抛出特定异常时才成功的测试支持

//可以指定一个异常
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}
//可以指定多个异常
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest2 {
    Class<? extends Exception>[] value();
}
//JDK8还有一种可以执行多值注解的用法:你可以在注解声明上使用 @Repeatable 元注解,以表明注解可以重复地应用于单个元素,而不是使用数组参数来声明注解类型。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Exception> value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}
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 wrappedEx) {
                    Throwable exc = wrappedEx.getCause();
                    Class<? extends Throwable> 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);
                    }
                }
                catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }
            //多异常
            if (m.isAnnotationPresent(ExceptionTest2.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception%n", m);
                } catch (Throwable wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    int oldPassed = passed;
                    Class<? extends Exception>[] excTypes =m.getAnnotation(ExceptionTest.class).value();
                    for (Class<? extends Exception> excType : excTypes) {
                        if (excType.isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPassed)
                        System.out.printf("Test %s failed: %s %n", m, exc);
                }
            }
            //JDK8多异常
            if (m.isAnnotationPresent(ExceptionTest.class)|| m.isAnnotationPresent(ExceptionTestContainer.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception%n", m);
                } catch (Throwable wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    int oldPassed = passed;
                    ExceptionTest[] excTests =m.getAnnotationsByType(ExceptionTest.class);
                    for (ExceptionTest excTest : excTests) {
                        if (excTest.value().isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPassed)
                        System.out.printf("Test %s failed: %s %n", m, exc);
                }
            }
        }
    	System.out.printf("Passed: %d, Failed: %d%n",passed, tests - passed);
    }
}

处理可重复注解需要小心。
「重复状态」会生成名为「容器注解类型」的合成注解。
getAnnotationsByType 方法可忽略这一区别,它可以用于访问可重复注解类型的「重复状态」和「非重复状态」。
但是 isAnnotationPresent 明确指出,「重复状态」的情况不属于注解类型,而是「容器注解类型」。
如果一个元素是某种类型的「重复状态」注解,并且你使用 isAnnotationPresent 方法检查该元素是否具有该类型的注解,你将发现它提示不存在。
因此,使用此方法检查注解类型的存在与否,将导致你的程序忽略「重复状态」。
类似地,使用此方法检查「容器注解类型」将导致程序忽略「非重复状态」。
要使用 isAnnotationPresent 检测「重复状态」和「非重复状态」,需要同时检查注解类型及其「容器注解类型」。

即如上的ExceptionTestContainer为容器注解类型;
if判断中使用||来保障

40.始终如一地使用@Override

此注解将减少受到有害错误的影响

41.使用标记接口来定义类型

标记接口是一种不包含任何方法声明的接口,它只是指定(或标记)一个类,该类实现了具有某些属性的接口。
例如,考虑 Serializable 接口。通过实现此接口,表示类的实例可以写入 ObjectOutputStream(或序列化)。

与标记注解相比,标记接口有两个优点:
首先,标记接口定义的类型由标记类的实例实现;标记注解不会。 标记接口类型的存在允许你在编译时捕获错误,如果你使用标记注解,则在运行时才能捕获这些错误。
第二个优点是可以更精确地定位它们

相对于标记接口,标记注解的主要优势是它们可以是其他注解功能的一部分。 因此,标记注解能够与基于使用注解的框架保持一致性。

什么时候应该使用标记注解,什么时候应该使用标记接口?
显然,如果标记应用于类或接口之外的任何程序元素,则必须使用标记注解,因为只有类和接口才能实现或扩展接口。
如果标记只适用于类和接口,那么可以问一个问题:「我是否想编写一个或多个只接受具有此标记的对象的方法?」如果是这样,你应该使用标记接口而不是标记注解。这样能够将接口用作相关方法的参数类型,这将带来编译时类型检查的好处。
如果永远不会编写只接受带有标记的对象的方法,那么最好使用标记注解。
此外,如果框架大量使用注解,那么标记注解就是明确的选择。

与22相呼应:
如果不想定义类型,就不要使用接口,如常量接口就是种不好的模式;
如果你确实想定义类型,那么就要使用接口

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源
大学生在线租房平台管理系统按照操作主体分为管理员和用户。管理员的功能包括报修管理、报修评价管理、字典管理、房东管理、房屋管理、房屋收藏管理、房屋留言管理、房屋租赁管理、租房论坛管理、公告信息管理、留言板管理、用户管理、管理员管理。用户的功能等。该系统采用了Mysql数据库,Java语言,Spring Boot框架等技术进行编程实现。 大学生在线租房平台管理系统可以提高大学生在线租房平台信息管理问题的解决效率,优化大学生在线租房平台信息处理流程,保证大学生在线租房平台信息数据的安全,它是一个非常可靠,非常安全的应用程序。 管理员权限操作的功能包括管理公告,管理大学生在线租房平台信息,包括房屋管理,培训管理,报修管理,薪资管理等,可以管理公告。 房屋管理界面,管理员在房屋管理界面可以对界面显示,可以对房屋信息的房屋状态进行查看,可以添加新的房屋信息等。报修管理界面,管理员在报修管理界面查看报修种类信息,报修描述信息,新增报修信息等。公告管理界面,管理员在公告管理界面新增公告,可以删除公告。公告类型管理界面,管理员在公告类型管理界面查看公告的工作状态,可以对公告的数据进行导出,可以添加新公告的信息,可以编辑公告信息,删除公告信息。
基于hal库的OLED显示屏驱动C语言实现源码.zip 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我! 基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip基于hal库的OLED显示屏驱动C语言实现源码.zip
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值