《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. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值