Java编程思想 枚举类型总结

关键字enum可以将一组具名的值的有限集合创建为一种新的类型 而这些具名的值可以作为常规的程序组件使用 这是一种非常有用的功能

基本enum特性
创建enum时 编译器会为你生成一个相关的类 这个类继承自java.lang.Enum 下面的例子演示了Enum提供的一些功能
Java编程思想 枚举类型总结_第1张图片
Java编程思想 枚举类型总结_第2张图片

将静态导入用于enum
Burrito.java的另一个版本
Java编程思想 枚举类型总结_第3张图片
使用static import能够将enum实例的标识符带入当前的命名空间 所以无需再用enum类型来修饰enum实例 这是一个好的想法吗 或者还是显式地修饰enum实例更好 这要看代码的复杂程度了 编译器可以确保你使用的是正确的类型 所以唯一需要担心的是 使用静态导入会不会导致你的代码令人难以理解 多数情况下 使用static import还是有好处的 不过 程序员还是应该对具体情况进行具体分析
注意 在定义enum的同一个文件中 这种技巧无法使用 如果是在默认包中定义enum 这种技巧也无法使用

向enum中添加新方法
除了不能继承自一个enum之外 我们基本上可以将enum看作一个常规的类 也就是说 我们可以向enum中添加方法 enum甚至可以有main()方法
一般来说 我们希望每个枚举实例能够返回对自身的描述 而不仅仅只是默认的toString()实现 这只能返回枚举实例的名字 为此 你可以提供一个构造器 专门负责处理这个额外的信息 然后添加一个方法 返回这个描述信息 看一看下面的示例
Java编程思想 枚举类型总结_第4张图片
注意 如果你打算定义自己的方法 那么必须在enum实例序列的最后添加一个分号 同时 Java要求你必须先定义enum实例 如果在定义enum实例之前定义了任何方法或属性 那么在编译时就会得到错误信息

覆盖enum的方法
覆盖toString()方法 给我们提供了另一种方式来为枚举实例生成不同的字符串描述信息 在下面的示例中 我们使用的就是实例的名字 不过我们希望改变其格式 覆盖enum的toSting()方法与覆盖一般类的方法没有区别
Java编程思想 枚举类型总结_第5张图片
Java编程思想 枚举类型总结_第6张图片

switch语句中的enum
在switch中使用enum 是enum提供的一项非常便利的功能 一般来说 在switch中只能使用整数值 而枚举实例天生就具备整数值的次序 并且可以通过ordinal()方法取得其次序(显然编译器帮我们做了类似的工作) 因此我们可以在switch语句中使用enum
虽然一般情况下我们必须使用enum类型来修饰一个enum实例 但是在case语句中却不必如此 下面的例子使用enum构造了一个小型状态机
Java编程思想 枚举类型总结_第7张图片

values()的神秘之处
编译器为你创建的enum类都继承自Enum类 然而 如果你研究一下Enum类就会发现 它并没有values()方法 可我们明明已经用过该方法了 难道存在某种 隐藏的 方法吗 我们可以利用反射机制编写一个简单的程序 来查看其中的究竟
Java编程思想 枚举类型总结_第8张图片
Java编程思想 枚举类型总结_第9张图片
答案是 values()是由编译器添加的static方法 可以看出 在创建Explore的过程中 编译器还为其添加了valueOf()方法 这可能有点令人迷惑 Enum类不是已经有valueOf()方法了吗 不过Enum中的valueOf()方法需要两个参数 而这个新增的方法只需一个参数 由于这里使用的Set只存储方法的名字 而不考虑方法的签名 所以在调用Explore.removeAll(Enum)之后 就只剩下[values]了

由于values()方法是由编译器插入到enum定义中的static方法 所以 如果你将enum实例向上转型为Enum 那么values()方法就不可访问了 不过 在Class中有一个getEnumConstants()方法 所以即便Enum接口中没有values()方法 我们仍然可以通过Class对象取得所有enum实例
Java编程思想 枚举类型总结_第10张图片
因为getEnumConstants()是Class上的方法 所以你甚至可以对不是枚举的类调用此方法
Java编程思想 枚举类型总结_第11张图片
只不过 此时该方法返回null 所以当你试图使用其返回的结果时会发生异常

实现 而非继承
我们已经知道 所有的enum都继承自java.lang.Enum类 由于Java不支持多重继承 所以你的enum不能再继承其他类
在这里插入图片描述
然而 在我们创建一个新的enum时 可以同时实现一个或多个接口
Java编程思想 枚举类型总结_第12张图片
这个结果有点奇怪 不过你必须要有一个enum实例才能调用其上的方法 现在 在任何接受Generator参数的方法中 例如printNext() 都可以使用CartoonCharacter

随机选取
就像你在CartoonCharacter.next()中看到的那样 本章中的很多示例都需要从enum实例中进行随机选择 我们可以利用泛型 从而使得这个工作更一般化 并将其加入到我们的工具库中
Java编程思想 枚举类型总结_第13张图片
在这里插入图片描述
古怪的语法表示T是一个enum实例 而将Class作为参数的话 我们就可以利用Class对象得到enum实例的数组了 重载后的random()方法只需使用T[]作为参数 因为它并不会调用Enum上的任何操作 它只需从数组中随机选择一个元素即可 这样 最终的返回类型正是enum的类型
下面是random()方法的一个简单示例
Java编程思想 枚举类型总结_第14张图片
虽然Enum只是一个相当短小的类 但是你会发现 它能消除很多重复的代码 重复总会制造麻烦 因此消除重复总是有益处的

使用接口组织枚举
无法从enum继承子类有时很令人沮丧 这种需求有时源自我们希望扩展原enum中的元素 有时是因为我们希望使用子类将一个enum中的元素进行分组
在一个接口的内部 创建实现该接口的枚举 以此将元素进行分组 可以达到将枚举元素分类组织的目的 举例来说 假设你想用enum来表示不同类别的食物 同时还希望每个enum元素仍然保持Food类型 那可以这样实现
Java编程思想 枚举类型总结_第15张图片
对于enum而言 实现接口是使其子类化的唯一办法 所以嵌入在Food中的每个enum都实现了Food接口 现在 在下面的程序中 我们可以说 所有东西都是某种类型的Food
Java编程思想 枚举类型总结_第16张图片
如果enum类型实现了Food接口 那么我们就可以将其实例向上转型为Food 所以上例中的所有东西都是Food
然而 当你需要与一大堆类型打交道时 接口就不如enum好用了 例如 如果你想创建一个 枚举的枚举 那么可以创建一个新的enum 然后用其实例包装Food中的每一个enum类
Java编程思想 枚举类型总结_第17张图片
在上面的程序中 每一个Course的实例都将其对应的Class对象作为构造器的参数 通过getEnumConstants()方法 可以从该Class对象中取得某个Food子类的所有enum实例 这些实例在randomSelection()中被用到 因此 通过从每一个Course实例中随机地选择一个Food 我们便能够生成一份菜单
Java编程思想 枚举类型总结_第18张图片
Java编程思想 枚举类型总结_第19张图片
在这个例子中 我们通过遍历每一个Course实例来获得 枚举的枚举 的值 稍后 在VendingMachine.java中 我们会看到另一种组织枚举实例的方式 但其也有一些其他的限制
此外 还有一种更简洁的管理枚举的办法 就是将一个enum嵌套在另一个enum内 就像这样
Java编程思想 枚举类型总结_第20张图片
Security接口的作用是将其所包含的enum组合成一个公共类型 这一点是有必要的 然后 SecurityCategory才能将Security中的enum作为其构造器的参数使用 以起到组织的效果
如果我们将这种方式应用于Food的例子 结果应该这样
Java编程思想 枚举类型总结_第21张图片
其实 这仅仅是重新组织了一下代码 不过多数情况下 这种方式使你的代码具有更清晰的结构

使用EnumSet替代标志
Set是一种集合 只能向其中添加不重复的对象 当然 enum也要求其成员都是唯一的 所以enum看起来也具有集合的行为 不过 由于不能从enum中删除或添加元素 所以它只能算是不太有用的集合 Java SE5引入EnumSet 是为了通过enum创建一种替代品 以替代传统的基于int的 位标志 这种标志可以用来表示某种 开/关 信息 不过 使用这种标志 我们最终操作的只是一些bit 而不是这些 bit想要表达的概念 因此很容易写出令人难以理解的代码

EnumSet中的元素必须来自一个enum 下面的enum表示在一座大楼中 警报传感器的安放位置
在这里插入图片描述
然后 我们用EnumSet来跟踪报警器的状态
Java编程思想 枚举类型总结_第22张图片

EnumSet的基础是long 一个long值有64位 而一个enum实例只需一位bit表示其是否存在 也就是说 在不超过一个long的表达能力的情况下 你的EnumSet可以应用于最多不超过64个元素的enum 如果enum超过了64个元素会发生什么呢
Java编程思想 枚举类型总结_第23张图片
显然 EnumSet可以应用于多过64个元素的enum 所以猜测 Enum会在必要的时候增加一个long

使用EnumMap
EnumMap是一种特殊的Map 它要求其中的键(key)必须来自一个enum 由于enum本身的限制 所以EnumMap在内部可由数组实现 因此EnumMap的速度很快 我们可以放心地使用enum实例在EnumMap中进行查找操作 不过 我们只能将enum的实例作为键来调用put()方法 其他操作与使用一般的Map差不多
下面的例子演示了命令设计模式的用法 一般来说 命令模式首先需要一个只有单一方法的接口 然后从该接口实现具有各自不同的行为的多个子类 接下来 程序员就可以构造命令对象 并在需要的时候使用它们了
Java编程思想 枚举类型总结_第24张图片
Java编程思想 枚举类型总结_第25张图片
与EnumSet一样 enum实例定义时的次序决定了其在EnumMap中的顺序
main()方法的最后部分说明 enum的每个实例作为一个键 总是存在的 但是 如果你没有为这个键调用put()方法来存入相应的值的话 其对应的值就是null
与常量相关的方法(constant specific methods)相比 EnumMap有一个优点 那EnumMap允许程序员改变值对象 而常量相关的方法在编译期就被固定了
稍后你会看到 在你有多种类型enum 而且它们之间存在互操作的情况下 我们可以用EnumMap实现多路分发(multiple dispatching)

常量相关的方法
Java的enum有一个非常有趣的特性 即它允许程序员为enum实例编写方法 从而为每个enum实例赋予各自不同的行为 要实现常量相关的方法 你需要为enum定义一个或多个abstract方法 然后为每个enum实例实现该抽象方法 参考下面的例子
Java编程思想 枚举类型总结_第26张图片
通过相应的enum实例 我们可以调用其上的方法 这通常也称为表驱动的代码(table driven code 请注意它与前面提到的命令模式的相似之处)
在面向对象的程序设计中 不同的行为与不同的类关联 而通过常量相关的方法 每个enum实例可以具备自己独特的行为 这似乎说明每个enum实例就像一个独特的类 在上面的例子中 enum实例似乎被当作其 超类 ConstantSpecificMethod来使用 在调用getInfo()方法时 体现出多态的行为
然而 enum实例与类的相似之处也仅限于此了 我们并不能真的将enum实例作为一个类型来使用
Java编程思想 枚举类型总结_第27张图片

再看一个更有趣的关于洗车的例子 每个顾客在洗车时 都有一个选择菜单 每个选择对应一个不同的动作 可以将一个常量相关的方法关联到一个选择上 再使用一个EnumSet来保存客户的选择
Java编程思想 枚举类型总结_第28张图片
Java编程思想 枚举类型总结_第29张图片
与使用匿名内部类相比较 定义常量相关方法的语法更高效 简洁

除了实现abstract方法以外 程序员是否可以覆盖常量相关的方法呢 答案是肯定的 参考下面的程序
Java编程思想 枚举类型总结_第30张图片
Java编程思想 枚举类型总结_第31张图片
虽然enum有某些限制 但是一般而言 我们还是可以将其看作是类

使用enum的职责链
在职责链(Chain of Responsibility)设计模式中 程序员以多种不同的方式来解决一个问题 然后将它们链接在一起 当一个请求到来时 它遍历这个链 直到链中的某个解决方案能够处理该请求
通过常量相关的方法 我们可以很容易得实现一个简单的职责链 我们以一个邮局的模型为例 邮局需要以尽可能通用的方式来处理每一封邮件 并且要不断尝试处理邮件 直到该邮件最终被确定为一封死信 其中的每一次尝试可以看作为一个策略(也是一个设计模式) 而完整的处理方式列表就是一个职责链
我们先来描述一下邮件 邮件的每个关键特征都可以用enum来表示 程序将随机地生成Mail对象 如果要减小一封邮件的GeneralDelivery为YES的概率 那最简单的方法就是多创建几个不是YES的enum实例 所以enum的定义看起来有点古怪
我们看到Mail中有一个randomMail()方法 它负责随机地创建用于测试的邮件 而generator()方法生成一个Iterable对象 该对象在你调用next()方法时 在其内部使用randomMail()来创建Mail对象 这样的结构使程序员可以通过调用Mail.generator()方法 很容易地构造出一个foreach循环
Java编程思想 枚举类型总结_第32张图片
Java编程思想 枚举类型总结_第33张图片
Java编程思想 枚举类型总结_第34张图片
Java编程思想 枚举类型总结_第35张图片
Java编程思想 枚举类型总结_第36张图片
在这里插入图片描述
职责链由enum MailHandler实现 而enum定义的次序决定了 各个解决策略在应用时的次序 对每一封邮件 都要按此顺序尝试每个解决策略 直到其中一个能够成功地处理该邮件 如果所有的策略都失败了 那么该邮件将被判定为一封死信

使用enum的状态机
枚举类型非常适合用来创建状态机 一个状态机可以具有有限个特定的状态 它通常根据输入 从一个状态转移到下一个状态 不过也可能存在瞬时状态(transient states) 而一旦任务执行结束 状态机就会立刻离开瞬时状态
每个状态都具有某些可接受的输入 不同的输入会使状态机从当前状态转移到不同的新状态 由于enum对其实例有严格限制 非常适合用来表现不同的状态和输入 一般而言 每个状态都具有一些相关的输出
自动售货机是一个很好的状态机的例子 首先 我们用一个enum定义各种输入
Java编程思想 枚举类型总结_第37张图片

VendingMachine对输入的第一个反应是将其归类为Category enum中的某个enum实例 这可以通过switch实现 下面的例子演示了enum是如何使代码变得更加清晰且易于管理的
Java编程思想 枚举类型总结_第38张图片
Java编程思想 枚举类型总结_第39张图片
Java编程思想 枚举类型总结_第40张图片
Java编程思想 枚举类型总结_第41张图片
Java编程思想 枚举类型总结_第42张图片

通过两种不同的Generator对象 我们可以用两种方式来测试VendingMachine 首先是RandomInputGenerator 它会不停地生成各种输入 当然 除了SHUT_DOWN之外 通过长时间地运行RandomInputGenerator 可以起到健全测试(sanity test)的作用 能够确保该状态机不会进入一个错误状态 另一个是FileInputGenerator 使用文件以文本的方式来描述输入 然后将它们转换成enum实例 并创建对应的Input对象 上面的程序使用的正是如下的文本文件
Java编程思想 枚举类型总结_第43张图片
这种设计有一个缺陷 它要求enum State实例访问的VendingMachine属性必须声明为static 这意味着 你只能有一个VendingMachine实例 不过如果我们思考一下实际的(嵌入式Java)应用 这也许并不是一个大问题 因为在一台机器上 我们可能只有一个应用程序

多路分发
当你要处理多种交互类型时 程序可能会变得相当杂乱 举例来说 如果一个系统要分析和执行数学表达式 我们可能会声明Number.plus(Number) Number.multiple(Number)等等 其中Number是各种数字对象的超类 然而 当你声明a.plus(b)时 你并不知道a或b的确切类型 那你如何能让它们正确地交互呢
你可能从未思考过这个问题的答案 Java只支持单路分发 也就是说 如果要执行的操作包含了不止一个类型未知的对象时 那么Java的动态绑定机制只能处理其中一个的类型 这就无法解决我们上面提到的问题 所以 你必须自己来判定其他的类型 从而实现自己的动态绑定行为
解决上面问题的办法就是多路分发(在那个例子中 只有两个分发 一般称之为两路分发) 多态只能发生在方法调用时 所以 如果你想使用两路分发 那么就必须有两个方法调用 第一个方法调用决定第一个未知类型 第二个方法调用决定第二个未知的类型 要利用多路分发 程序员必须为每一个类型提供一个实际的方法调用 如果你要处理两个不同的类型体系 就需要为每个类型体系执行一个方法调用 一般而言 程序员需要有设定好的某种配置 以便一个方法调用能够引出更多的方法调用 从而能够在这个过程中处理多种类型 为了达到这种效果 我们需要与多个方法一同工作 因为每个分发都需要一个方法调用 在下面的例子中(实现了 石头 剪刀 布 游戏 也称为RoShamBo)对应的方法是compete()和eval() 二者都是同一个类型的成员 它们可以产生三种Outcome实例中的一个作为结果
Java编程思想 枚举类型总结_第44张图片
Java编程思想 枚举类型总结_第45张图片
Java编程思想 枚举类型总结_第46张图片

使用enum分发
直接将RoShamBo1.java翻译为基于enum的版本是有问题的 因为enum实例不是类型 不能将enum实例作为参数的类型 所以无法重载eval()方法 不过 还有很多方式可以实现多路分发 并从enum中获益
一种方式是使用构造器来初始化每个enum实例 并以 一组 结果作为参数 这二者放在一块 形成了类似查询表的结构
Java编程思想 枚举类型总结_第47张图片
Java编程思想 枚举类型总结_第48张图片

在代码中 enum被单独抽取出来 因此它可以应用在其他例子中 首先 Competitor接口定义了一种类型 该类型的对象可以与另一个Competitor相竞争
Java编程思想 枚举类型总结_第49张图片
然后 我们定义两个static方法(static可以避免显式地指明参数类型) 第一个是match()方法 它会为一个Competitor对象调用compete()方法 并与另一个Competitor对象作比较 在这个例子中 我们看到 match()方法的参数需要是Competitor类型 但是在play()方法中 类型参数必须同时是Enum类型(因为它将在Enums.random()中使用)和Competitor类型(因为它将被传递给match()方法)
Java编程思想 枚举类型总结_第50张图片
play()方法没有将类型参数T作为返回值类型 因此 似乎我们应该在Class中使用通配符来代替上面的参数声明 然而 通配符不能扩展多个基类 所以我们必须采用以上的表达式

使用常量相关的方法
常量相关的方法允许我们为每个enum实例提供方法的不同实现 这使得常量相关的方法似乎是实现多路分发的完美解决方案 不过 通过这种方式 enum实例虽然可以具有不同的行为 但它们仍然不是类型 不能将其作为方法签名中的参数类型来使用 最好的办法是将enum用在switch语句中 见下例
Java编程思想 枚举类型总结_第51张图片
虽然这种方式可以工作 但是却不甚合理 如果采用RoShamBo2.java的解决方案 那么在添加一个新的类型时 只需更少的代码 而且也更直接
然而 RoShamBo3.java还可以压缩简化一下
Java编程思想 枚举类型总结_第52张图片
Java编程思想 枚举类型总结_第53张图片
其中 具有两个参数的compete()方法执行第二个分发 该方法执行一系列的比较 其行为类似switch语句 这个版本的程序更简短 不过却比较难理解 对于一个大型系统而言 难以理解的代码将导致整个系统不够健壮

使用EnumMap分发
使用EnumMap能够实现 真正的 两路分发 EnumMap是为enum专门设计的一种性能非常好的特殊Map 由于我们的目的是摸索出两种未知的类型 所以可以用一个EnumMap的EnumMap来实现两路分发
Java编程思想 枚举类型总结_第54张图片
该程序在一个static子句中初始化EnumMap对象 具体见表格似的initRow()方法调用 请注意compete()方法 您可以看到 在一行语句中发生了两次分发

使用二维数组
我们还可以进一步简化实现两路分发的解决方案 我们注意到 每个enum实例都有一个固定的值(基于其声明的次序) 并且可以通过ordinal()方法取得该值 因此我们可以使用二维数组 将竞争者映射到竞争结果 采用这种方式能够获得最简洁 最直接的解决方案(很可能也是最快速的 虽然我们知道EnumMap内部其实也是使用数组实现的)
Java编程思想 枚举类型总结_第55张图片

你可能感兴趣的:(Java编程思想)