《Effective Java》第6章 枚举与注解

时间:2023-02-15 18:33:46

Jdk 1.5发行版本新增了两个引用类型家族:一种新的称作枚举类型(enum);另一种新的接口称作注解类型(annotation type)

  1. 用枚举代替int常量【Item 30】
    1)枚举类型是指由一组固定的常量组成合法值的类型,如一年中的季节,一副牌的花色,一星期中的每天名称等
    2)int枚举模式是指声明一组具名的int常量,每个类型成员一个常量,而int枚举是编译时常量,会被编译到它们的客户端中,如果与枚举常量关联的int发生了辩护,客户端就必须重新编译,否则运行时的行为是不确定的,且其打印结果是完全不知名的int值,可读性差
    3)String枚举模式是指声明一组具名的String常量,虽然增加了打印输出内容的可读性,但是这种常量会导致性能问题,因为它依赖于字符串的比较操作,更糟糕的是这种常量如果字符串常量中有拼写错误在编译时是无法发现的,到运行时会报错
    4)枚举类型解决了int/String枚举模式的缺点,并提供了一些强大的功能。Java枚举类型是通过公有的静态final域为每个枚举常量导出实例的类,因为没有可访问的构造器,枚举类型是真正的final,且枚举类型为类型安全的枚举模式,其提供了编译时的类型安全
    5)包含同名常量的多个枚举类型可以在一个系统中和平相处,因为每个类型都有自己的命名空间。如果增加或者重新排列了枚举类型中的常量,无需重新编译它的客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层,常量并没有被编译到客户端代码中,而是在ini枚举模式之中
    6)为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器,枚举天生不可变,因此所有域都应该为final的,并且尽量缩小可访问性
    7)所有枚举都有一个静态的values方法,按照声明的顺序返回它的值数组,可以用for-each来遍历枚举类型的枚举常量,;枚举类型中的枚举常量的toString方法返回每个枚举值的声明名称;枚举类型有一个自动产生的valuesOf(String)方法,它将常量的名字转变成为常量本身
    8)如果一个枚举类具有普遍适用性,它就应该成为一个顶层类;如果它只是被用在一个特定的顶层类中,它就应该成为该顶层类的一个成员类
    9)枚举类型中的常量都可以看做一个类,其可以有类构造方法,可以有类主体,也就是说可以有自身的长远方法以及域等
    10)特定于常量的方法实现是指在枚举类型中声明一个抽象方法,并在特定于常量的类主体中用具体的方法覆盖该抽象方法,这样就为每个常量增加了特定的行为。且枚举类型中的所有常量必须在其类主体中覆盖枚举类型中的抽象方法,类似与策略模式,每个常量中的具体方法实现是一种策略
    11)在枚举类型中覆盖了toString方法,考虑增加一个fromString方法讲定制的字符串变回相应的枚举
    12)枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为,并不适合于在枚举中实现特定于常量的行为
    13)枚举会优先使用comparable而非int常量,与int常量相比,枚举有个小小的性能缺点,即装载和初始化枚举时会有空间和时间的成本,在有设备资源约束的情况下需要考虑
    14)大多数枚举不需要有显式的构造器或者成员,少数受益于“每个常量与属性的关联”以及“提供行为受这个属性影响的方法”;极少数枚举受益于将多种行为与单个方法关联,这种情况下,特定于常量的方法要优先于启用自有值的枚举;如果多个枚举常量同时共享相同的行为,则考虑策略枚举类型
  2. 用实例域代替序数【Item 31】
    1)所有枚举有一个ordinal方法,它返回每个枚举常量在类型中的数字位置
    2)永远不要根据枚举的序数来导出与它关联的值,而是要将它保存在一个实例域中,即每个常量的类主体中存一个与此常量相关的数据,这样常量的变更会同样带动与之关联值的变动,确保关联值的正确性
    3)Enum规范中对ordinal方法的说明是大多数程序要都不需要使用这个方法,它是设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构,除非你在编写的是这种数据结构,否则完全避免使用该方法
  3. 用EnumSet代替位域【Item 32】
    1)位域,即用int常量来初始化一个个int常量值,通过位运算来提升性能以及代码简洁性
    2)正是因为枚举类型要用在集合(Set)中,所以没有理由用位域来表示它,EnumSet类集位域的简洁和性能优势以及枚举类型的所有优点于一身,但是截止jdk 1.6,都无法创建不可变EnumSet是其唯一缺点
    3)整合EnumSet就是用单个long来表示,因此它的性能比得上位域的性能,批处理removeAll和retainAll都是利用位算法来实现的,就像手工替位域实现的那样,避免了手工位操作的容易出现的错误以及不太雅观的代码,这项任务已经由EnumSet接手
  4. 用EnumMap代替序数索引【Item 33】
    1)一旦想要在枚举中通过序数来作为key关联相关的vlaue时,更加安全的方式是使用EnumMap,并非可能绕过编译时错误检查的手动关联数组索引等方式
    2)最好不要用序数来索引数组,而要使用EnumMap。如果你所表示的这种关系是多维的,就使用EnumMap<…, EnumMap<…>>
  5. 用接口模拟可伸缩的枚举【Item 34】
    1)虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。这样允许客户端编写自己的枚举来实现接口。如果API是根据接口编写的,那么在可以使用基础枚举类型的任何地方,也都可以使用这些枚举
    2)用接口模拟可伸缩枚举有个小小的不足,即无法将实现从一个枚举类型继承到另一个枚举类型
    3)接口模拟可扩展的实现方式就是在枚举类型上implements一个接口
  6. 注解优先于命名模式【Item 35】
    1)jdk 1.5版本之前一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理,例如JUit猜测是框架要求它的用户一定要用test作为测试方法名称的开头
    2)命名模式有三大缺点
    a.文字拼写错误会导致失败,且没有任何提示
    b.无法确保它们只用于相应的程序元素上
    c.它们没有提供将参数值与程序元素关联起来的好方法
    3)注解永远不会改变被注解代码的语义,但是它可以通过工具进行特殊的处理,例如测试代码的注解有助于测试程序对测试方法的检测
    4)既然有了注解,就完全没有必要再使用命名模式了
    5)大多数程序员不必定义注解类型,但是所有的程序员都应该使用Java平台所提供的预定义的注解类型,还要考虑使用IDE或者静态分析工具所提供的任何注解
    6)注解也是一种类型
  7. 坚持使用Override注解【Item 36】
    1)随着Java 1.5版本中增加了注解,类库中也增加了几种注解类型,如Override,SuppressWarnings等
    2)应该在所有覆盖超类中的方法声明中使用Override注解,这样在编译时能帮忙明确的检测出一些错误
    3)由于在extends/implements抽象类或者接口时,如果在具体类中没有去覆盖抽象方法编译器也会报错,所以这种情况可以不用标注Override,但是如果想要用该注解来标注该方法是从超类或接口中继承过来的,可以用Override注解来区分
  8. 用标记接口定义类型【Item 37】
    1)标记接口是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口
    2)标记接口定义的类型是由被标记类的实例实现的,而标记注解则没有定义这样的类型
    3)如果想要定义一个任何新方法都不会与之关联的类型,标记接口是最好的选择;如果想要标记程序元素而非类和接口,考虑未来可能要给标记添加更多的信息,或者标记要适合于已经广泛使用了注解类型的框架,那么标记注解是正确的选择
    4)如果你发现自己在编写的是目标为ElementType.TYPE的标记注解类型,就应该花点时间考虑清楚,它是否真的应该为注解类型,是不是可以通过标记接口来实现,毕竟元素类型为类型
    5)如果不想定义类型就不要使用接口,也就是说如果想要定义类型,就一定要使用接口