【读书笔记】《Effective Java》(5)--枚举和注解

时隔几日,我又总结了一章内容。这一章是枚举和注解,都是Java1.5新加入的特性

枚举和注解

枚举类型enum是Java1.5加入的类型,在之前的代码中没有,所以《Effective Java》第一版中提到了“类型安全的枚举”,“类型安全的枚举”是指使用一个自定义的类,类中定义公有静态的int型表示枚举类型。

Java中的枚举和C++的不一样,因为我有C++的基础,起初看Java的枚举的时候有些迷糊。Java的枚举内部是通过继承一个Enum的类实现的(所以枚举对象不能再继承类了,但是能扩展接口),所以Java的枚举更像一个功能完善的类,甚至自己实现了Serializable接口和Comparable接口。

个人感觉,总的来说,这一部分的内容没有前面部分来的深入,一方面原因应当是这部分内容是Java1.5新加入的特性,另一方面还有枚举和注解在实际使用中用到的没有那么多吧。

30. 用enum代替int常量

  • int枚举模式:声明一组int型常量,用作表示固定的,个数有限且已知的几个值

    缺点:int本身是可变的,可以轻易地用于计算,而且编译器不会有任何提示,这破坏当作常量的初衷。其次,int型输出时仅仅是数字,没有携带任何它表示的常量的信息,这导致了不好调试。此外,int型枚举是编译时常量,一旦int变化了,客户代码需要重新编译,否则会出现错误。

  • String枚举模式:通过String型表示一系列的常量,比int枚举模式好的地方是输出携带了语义。

    缺点:字符串的比较较为损耗性能。字符串输入错误没有提示,容易出错。

  • enum的使用技巧:

    1. 特定于常量的方法实现:

      • 使用环境:枚举类有一个方法所有常量都要使用,但是担心在新编写常量的时候忘记更改这个方法。
      • 使用:在枚举类型中声明一个抽象的方法,然后在特定于常量的类主体中用具体的处理方法覆盖,这种情况下如果忘记编写新增添的常量的方法时,编译器会报错。
      • 例子:
        public enum Operation{
            PLUS{
                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;
                }
            };
            abstract double(double x,double y);
        }
    2. 覆盖toString 时考虑编写一个formString方法:

      • 使用环境:toString方法将常量输出为String。如果要重写这个方法,那么最好写一个fromString方法,以自定义的toString方法的输出作为输入,最后输出对应的枚举常量,其中可是使用valueOf方法作为辅助。
  • 注意事项:

    1. 将常量声明在枚举类的第一行,不这样做的话会导致编译失败。
    2. 如果枚举类编写了构造器,不要再构造器中调用静态成员域。因为枚举类常量被首先初始化,这时需要调用构造器,而此时静态域还没有初始化,调用的话会导致抛出空指针异常。
    3. 尽量避免在枚举类中使用switch语句。使用switch可能会导致在以后的代码修改中忘记几种case的情况,而这种错误是不好发现的。可以使用策略枚举(枚举类里嵌套一个私有枚举类)来避免,代码如下:
      
      enum PayrollDay {
          MONDAY(PayType.WEEKDAY),
          THESDAY(PayType.WEEKDAY),
          WEDNESDAY(PayType.WEEKDAY),
          THURSDAY(PayType.WEEKDAY),
          FRIDAY(PayType.WEEKDAY),
          SATURDAY(PayType.WEEKEND),
          SUNDAY(PayType.WEEKEND);
      
          private final PayType p;
          PayrollDay(PayType p) {
              this.p = p;
          }
          double pay(double hoursWorked, double payRate) {
              return payType.pay(hoursWorked, payRate);
          }
      
          private enum PayType {
              WEEKDAY {
                  double overtimePay(double hours, double payRate) {
                      return hours <= HOURS_PER_SHIFT ? 0 :
                          (hours - HOURS_PER_SHIFT) * payRate * 2;
                  }
              },
              WEEKEND {
                  double overtimePay(double hours, double payRate) {
                      return hours * payRate / 2;
                  }
              };
              private final static int HOURS_PER_SHIFT = 8;
              abstract double overtimePay(double hours, double Rate);
              double pay(double hoursWorked, double payRate) {
                  double basePay = hoursWorked * payRate;
                  return basePay + overtimePay(hoursWorked, payRate);
              }
          }
      }

31. 用实例域代替序数

  • 枚举有方法ordianl,可以返回每一个枚举常量在类型中的数字位置,但这个数字最好不被其他方法依赖,因为这个数字可以被更改,比如枚举常量重新排序。

  • 本条建议:永远不要根据枚举的叙述导出与它相关的值,而是将它保存在一个实例域中

  • 使用方法:使用构造器,传入需要指定的int参数,设置getter方法。


32. 用EnumSet代替位域

  • 位域,值使用一个一个的bit存储数据,通过位运算进行计算的结构,拥有良好的性能。

  • 本条建议:使用底层也是用bit储存数据的EnumSet储存数据,当枚举常量少于64时,整个Set都储存在一个long大小的空间上(相关内容还有一部分可参考第1条)。这既有位域的优点,又没有位域需要手动实现位运算麻烦。


33. 用EnumMap代替序数索引

  • 本条建议:当时用ordinal方法来索引数组时,应当考虑使用EnumMap替换掉这种方式。因为如果索引的数组元素是泛型集合,泛型和数组不能很好共存。此外,int的索引需要我们手动输入,如果有错误的话并不好显示出来。甚至对于多维数组,也可以使用Map套Map的方式处理。

  • EnumMap内部也是通过数组实现的,但是它隐藏了内部实现,方便了我们的操作。


34. 用接口模拟可伸缩的枚举

  • 备注:这一条建议和《Effective Java》第一版有联系,第一版中提到了“类型安全的枚举”(见这篇笔记的开头)。这一条建议和第一版关于“类型安全的枚举”的一个特点有呼应,“类型安全的枚举”可以用一个枚举类去扩展另一个枚举类,这被成为可伸缩(本书说这最终证明是不好的特性)。

  • 作为Java1.5新引入的枚举类型,枚举本身就是一个继承来的类,因此它不允许再次继承,只能扩展接口,因此,属于第一版的“类型安全的枚举”使用的特性不能用了,现在可以使用的是通过先定义一个接口,枚举类扩展这个接口,进而形成在这个接口之下的一系列枚举类,达到可伸缩的目的


35. 注解优先于命名模式

  • 以前的做法:

    1. 在没有引入注解之前,一般工具和框架都是通过硬性规定方法名来确定方法是否需要工具或框架支持的,例如JUnit(一个Java的自动测试框架)要求,每一个测试方法都要以test开头,否则不认方法是测试方法。这种模式成为“命名模式”。
  • 以前做法的缺点:

    1. 拼写错误会导致失败,而且没有任何提示,因为Junit更不认为这是一个测试方法。
    2. 无法确保它们只用于相应的程序元素上,Junit到底有没有执行我们想让执行的方法。
    3. 命名模式没有提供将参数值和程序元素关联起来的好方法。
  • 本条建议:使用注解吧,使用注解可以表明方法的意图,不在需要对方法名有任何硬性要求,有了注解,就不在有理由使用命名模式了。


36. 坚持使用Override注解

这一条建议基本无法违背,我们都已经在用了,使用@Override 可以帮助我们检查一些不易察觉的代码错误。这里只提一些边边角角的注意点。

  • 具体类中,不需要注解覆盖了抽象方法的声明,因为抽象方法不在具体类中覆盖的话,是会报错的,即便不是用注解也能及时发现。当然注解了也没有坏处。

37. 用标记接口定义类型

  • 标记接口是指接口内没有包含方法声明,而是仅仅指明了一个类实现了某种属性的接口。比如Serializable接口,通过扩展这个接口,类表明它的实例可以被写道ObjectOutputStream(虽然书上没有提,我还是觉得Cloneable接口也符合这种情况)。

  • 标注接口比标记注解(标记注解就是第35条提到的)好的地方:

    1. 标记接口定义的类型是由被标记的类的实例实现的;标记注解则没有定义这样的类型。这个类型允许我们在编译期间捕获到标记注解在运行时才能捕获的错误,具体来说就是标记接口只当了一个通用的约定,如果类没有扩展这个接口,则不可以使用这个约定。

      例如: 如果不扩展Serializable接口,则调用ObjectOutputStream.write(Object)必然会失败。

    2. 标记接口可以更精确地进行锁定。假设一个标记只适用于特殊接口的实现,则进行针对性的标记,而注解则适用于任何类或者接口。

      • 标记注解比标记接口好的地方:
    3. 标记注解可以通过默认的方式挑你家一个或者多个注解类型元素,给已被使用的注解类型增添更多的信息,将其完善成一个丰富的注解类型。而标记接口一旦公布则不好再增添方法。
    4. 标记注解是更大的注解机制的一部分,它们在同样支持注解的框架中具有一致性。
  • 标记接口和标记注解如何选择:
    如果要编写的是只接受这种标记的方法(作为形参的类型参数),或者要永远闲着之恶中标记只用于特殊接口的元素,那么应当使用标记接口,如果两个问题都是否定的,那么应当使用标记注解。


你可能感兴趣的:(Java,知识,java,读书笔记,Effective)