第六章、枚举和注解

Java 1.5发行版本新增了两个引用类型家族:枚举类型(Enumerate类)和注解类型(Annotation接口)。

第三十条、用enum代替int常量

  1. 枚举类型是指由一组固定的常量组成合法值的类型。替代之前的具名的int常量。
    public static final int APPLE_FUJI = 0;

    这种方式称作int枚举模式,存在诸多的不足:
    程序十分脆弱,因为int枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的int发生了变化,客户端就必须重新编译,否则,程序行为会变得不确定性。(还有个变体是String枚举模式)

  2. java的枚举类型是功能十分齐全的类,本质上是int值。
    基本想法是:通过公有的静态final域为每个枚举常量导出实例的类。枚举类型是实例受控的,它们是单例的泛型化,本质上是单元素的枚举

    枚举提供了编译时的类型安全,如果声明一个参数的类型为Apple,就可以保证,被传到该参数上的任何非null的对象引用一定属于三个有效的Apple值之一。试图传递类型错误的值时,会导致编译时错误。
    public enum Apple{FUJI,PIPPIN,GERNNY_SMITH}
    包含同名变量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间。

  3. 除了弥补int枚举常量的不足,枚举类型还允许添加任意的方法和域(近似于类),并实现任意的接口。它们提供了所有Object方法的高级实现,实现了Comparable和Serializable接口。我们为啥要将方法或者域加入到枚举类型中?将数据与它的常量关联起来。可以用任何适当的方法来增强枚举类型。

     /**
      * Created by laneruan on 2017/7/10.
      * 一个枚举类型的例子
      * 为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。
      */
     public class EnumPlanet {
         public enum Planet{
             MERCURY(3.302e+23,2.439e6),
             VENUS(4.8693+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;
             private final double radius;
             private final double surfaceGravity;
             private static final double G = 6.67300E-11;
     
             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;
             }
         }
         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.println("Weight on " + p +" is "+p.surfaceWeight(mass));
             }
         }
     }
    

    与枚举常量关联的有些行为,可能只需要用在定义了枚举的类或者包中,这种行为最好被实现成私有的或者包级私有的方法。
    如果一个枚举具有普遍适用性,它就应该成为一个顶层类。如果它是被用在一个特定的顶层类中,它就应该成为该顶层类的一个成员类。

  4. 特定于常量的方法实现:将不同的行为与每个枚举常量关联起来

     public enum Operation{
         PLUS{
             @Override
             double apply(double x, double y) {
                 return x+y;
             }
         },
         MINUS{
             @Override
             double apply(double x, double y) {
                 return x-y;
             }
         },
         TIMES{
             @Override
             double apply(double x, double y) {
                 return x*y;
             }
         },
         DIVIDE{
             @Override
             double apply(double x, double y) {
                 return x/y;
             }
         };
         abstract double apply(double x,double y),
     }
    
  5. 什么时候应该使用枚举类型?
    每当需要一组固定常量的时候。包括:天然的枚举类型;在编译的时候就知道其所有可能值的集合。

  6. 总结:与int常量相比,枚举类型的优势不言而喻:易读,更加安全。功能更加强大。许多枚举都不需要显式的构造器或者成员,但许多其他枚举类型则受益于“每个常量与属性的关联”以及“提供行为受这个属性影响的方法”。


第三十一条、用实例域代替序数

许多枚举天生就与一个单独的int值相关联,所有的枚举都有一个ordinal方法,它返回每个枚举常量在类型中的数字位置。可以试着从序数中得到关联的int值。最好避免使用ordinal方法。

永远不要根据枚举的序数导出与它想关联的值,而是要将它保存在一个实例域中:

    public enum Ensemble{
        SOLO(1),DUET(2),TRIO(3),QUARTET(4),QUINTET(5),
        SETET(6),SEPET(7),OCTET(8),NONET(9),DECTET(10);
        private final int numberOfMusicians;
        Ensemble(int size){
            this.numberOfMusicians = size;
        }
        public int numberOfMusicians(){return numberOfMusicians;}
    //   public int numberOfMusicians(){return ordinal()+1;}
    }

第三十二条、用EnumSet代替位域

需要传递多组常量集时,java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取多个值的多个集合,这个类实现了Set接口,提供了丰富的功能。

    class Text{
        public enum Style {BOLD,ITALIC,UNDERLINE,STRIKETHROUGH}
        public void applyStyles(Set<Style> styles){}
    }
    text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));

总结:正是因为枚举类型要用在集合中,所以没有理由用位域来表示他。


第三十三条、用EnumMap来代替序数索引

最好不要用序数来索引数组,而要使用EnumMap。

import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 假設有個香草的數組,表示一座花園中的植物,想要按照类型(一年生、两年生、多年生)
 * 进行组织之后将这些植物列出来。
 * 注意的是:EnumMap采用了键类型的Class对象构造器:这是一个有限制的类型令牌,
 * 提供了运行时的泛型信息。
 */
public class EnumMapHerb {
    public enum Type{ANNUAL,PERENNIAL,BIENNIAL}

    private final String name;
    private final Type type;

    EnumMapHerb(String name,Type type){
        this.name = name;
        this.type = type;
    }
    @Override
    public String toString(){
        return name;
    }
    public static void main(String[] args){
        EnumMapHerb[] garden = null;
        Map<Type,Set<EnumMapHerb>> herbByType =
                new EnumMap<Type, Set<EnumMapHerb>>(EnumMapHerb.Type.class);
        for(EnumMapHerb.Type t :EnumMapHerb.Type.values()){
            herbByType.put(t,new HashSet<EnumMapHerb>());
        }
        for(EnumMapHerb h:garden){
            herbByType.get(h.type).add(h);
        }
    }
}

第三十四条、用接口模拟可伸缩的枚举

  1. 以操作码opcode为例:它的元素表示在某种机器上的那些操作。有时候,要尽可能地让API的用户提供他们自己的操作,这样可以有效地扩展API提供的操作集。可以利用枚举类型来实现这种效果:

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

    虽然枚举类型BasicOperaion不是可扩展的,但是接口类型是可扩展的,你可以定义另一个枚举类型,它实现这个接口,并用这个新类型的实例代替基本类型。

     public enum ExtendOperation implements Operation{
         EXP("^"){
             public double apply(double x,double y){
                 return Math.pow(x,y);
             }
         },
         REMAINDER("%"){
             public double apply(double x,double y){
                 return x % y;
             }
         };
         private final String symbol;
         ExtendOperation(String symbol){
             this.symbol =  symbol;
         }
         @Override
         public String toString(){
             return symbol;
         }
     }
    

    以下是该类型的使用:

     public static void main(String[] args){
         double x = Double.parseDouble(args[0]);
         double y = Double.parseDouble(args[1]);
         test(ExtendOperation.class,x,y);
     }
     //很复杂的声明确保了对象既表示枚举又表示Operation的子类型
     private static <T extends Enum<T> & Operation> void test(
             Class<T> opSet,double x, double y) {
         for(Operation op :opSet.getEnumConstants()){
             System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));
         }
     }
    

    第二种使用方法是使用Collection<? extends Operation>,这是个有限制的通配符类型:

     public static void main(String[] args){
         double x = Double.parseDouble(args[0]);
         double y = Double.parseDouble(args[1]);
         test(Arrays.asList(ExtendOperation.values()),x,y);
     }
     private static void test(
             Collection<? extends Operation> opSet,double x, double y) {
         for(Operation op :opSet){
             System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));
         }
     }
    

这样得到的代码不复杂且更灵活:允许操作者将多个实现类型的操作合并到一起。

  1. 总结:虽然无法编写可扩展的枚举类型,但可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。

第三十五条、注解优先于命名模式(Naming Pattern)

  1. Java 1.5之前一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理。这种模式缺点很明显:文字拼写错误很容易发生且难以发觉;无法确保它们只用于相应的程序元素上;没有提供将参数值与程序元素相关联起来的好方法

  2. 注解很好地解决了这些问题:
    假设想要定义一个注解类型RunTests来指定简单的测试,它们自动运行,并在抛出异常时失败:

     import java.lang.annotation.*;
     @Retention(RetentionPolicy.RUNTIME) //表明注解应在运行时保留,元注解
     @Target(ElementType.METHOD)  //表明只有在方法中声明才是合法的,元注解
     public @interface RunTests {
     }
    
  3. 现实应用中的Test注解,称作标记注解(marker annotation)。

  4. 总结:大多数程序员不需要定义注解类型,所有的程序员都应该使用Java平台所提供的预定义的注解类型,还要考虑使用IDE或者静态分析工具所提供的任何注解。

第三十六条、坚持使用@Override注解

  1. Override注解只能在方法声明时使用,表示被注解的方法声明覆盖了超类中的一个声明。坚持使用这一注解,可以防止一大类的非法错误。

  2. 现代的IDE提供了坚持使用@Override的另一种理由:IDE具有自动检查功能,称作代码检验(code inspection),当有一个方法没有@Override注解却覆盖了超类方法时,IDE会产生一条警告提醒你警惕无意识的覆盖。

  3. 总结:如果你想要的每个方法声明中使用Override注解来覆盖超类声明,编译器就可以替你防止大量的错误。但有一个例外:在具体的类中,不必标注你确信覆盖了抽象方法声明的方法。

第三十七条、用标记接口定义类型

  1. 标记接口(Marker Interface)是没有包含方法声明的接口,只是指明(或者标明)一个类实现了具有某种属性的接口。例:Serializable接口,通过实现这个接口,类表明它的实例可以被写到ObjectOutputStream(即“被序列化”)

  2. 标记注解和标记接口:

    • 标记接口的优点:
      • 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。这个类型允许你在编译时捕捉在使用标记注解的情况要到运行时才能捕捉到的错误。
      • 他们可以更加精确地进行锁定。
    • 标记注解的优点:
      • 它可以通过默认的方式添加一个或者多个注解类型元素,给已被使用的注解类型添加更多的信息。随着时间的迁移,简单的标记注解可以演变成更加丰富的注解类型,这在标记接口中是不可能的;
      • 它们是更大的注解机制的一部分。因此,标记注解在那些支持注解作为编程元素之一的框架中同样具有一致性。
  3. 何时使用标记注解和标记接口?

    如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接可以用来实现或者扩展接口。如果标记只应用在类和接口,思考下:我要编写一个或者多个只接受有这种标记的方法吗?如果是这样,优先使用标记接口。如果不是,再思考下:是否要永远限制这个标记只用于特殊接口的元素?如果是,则优先使用标记接口。 最后选择使用标记注解。

  4. 总结:标记接口和标记注解都有用处,如何选择见上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 枚举类型是指由一组固定的常量组成合法值的类型,例如一年中的季节,太阳系中的行星或者一副牌中的花色。在编程语言...
    小小辉_710a阅读 1,423评论 0 0
  • 经典重读——亚马逊链接 笔记链接 导图: 笔记文本: Effective Java1 第2章 创建和销毁对象1.1...
    8c3c932b5ffd阅读 1,409评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 对象的创建与销毁 Item 1: 使用static工厂方法,而不是构造函数创建对象:仅仅是创建对象的方法,并非Fa...
    孙小磊阅读 1,956评论 0 3
  • 人生最快乐的事不是高官厚禄的担惊受怕,也不是舒适安逸的碌碌无为,更不是日复一日的枯燥生活。 我认为最快乐的事就是,...
    公子凉阅读 2,703评论 8 13