Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 363|回复: 0

[枚举学习]Java面向对象设计的最佳实践-枚举设计

[复制链接]
  • TA的每日心情
    开心
    2021-3-12 23:18
  • 签到天数: 2 天

    [LV.1]初来乍到

    发表于 2014-10-28 23:55:56 | 显示全部楼层 |阅读模式
    对枚举类型印象大多来自于C 语言,在 C 语言中,枚举类型是一个 HardCode (硬编码)类型,其使用价值并不大。因此,在 java 5 之前,枚举是被抛弃的。然而 Java 5 以后的版本开始对枚举进行支持,枚举的引入给 Java 世界带来了争议。    笔者比较赞同引入枚举,作为一门通用的静态编程语言,应该是海纳百川的(因此笔者赞成闭包进入Java 7 ),多途径实现功能。

    枚举是一种特殊的(受限制的)类,它具有以下特点:  1. 可列性
    2. 常量性
    3. 强类型
    4. 类的特性  留下一个问题-怎么利用这些枚举特点,更好为设计服务呢?根据这些特点,下面向大家分别得介绍设计技巧。
      
      
       
       
         
       

         
       
      
        一、 可列性
         在设计中,必须搞清楚枚举使用场景 。 枚举内部成员都是可列的,或者说固定的。这种硬编码的形式,看上去令人觉得不自在,不过这就是枚举。如果需要动态(不可列)的成员话,请不好使用枚举。

        JDK提供不少良好的可列性设计枚举。比如时间单位 java.util.concurrent.TimeUnit 和线程状态枚举 java.lang.Thread.State 。

    假设有一个游戏难度枚举,有三种难度NORMAL , MEDIUM, HARD
    Java代码 /**
    * 游戏中的难度枚举:NORMAL , MEDIUM, HARD
    * @author mercyblitz
    */ public enum Difficulty {
        NORMAL, MEDIUM, HARD //注意:枚举成员命名,请使用英文大写形式 }   如果要添加其他成员,只能通过硬编码的方法添加到枚举类。回到枚举 Difficulty,低版本的必定会影响二进制兼容性。对于静态语言来说,是无法避免的,不能认为是枚举的短处。

    二、 常量性
        之所以定性为常量性,是因为枚举是不能改变,怎么证明成员其不变呢?利用上面的Difficulty枚举为例,一段简单的代码得到其原型,如下:
    Java代码
    1. import java.lang.reflect.Field;
    2. /**
    3. * Difficulty 元信息
    4. * @author mercy
    5. */
    6. public class MetaDifficulty {
    7.    public static void main(String[] args) {
    8.         MetaDifficulty instance = new MetaDifficulty();
    9.         // 利用反射连接其成员的特性
    10.         Class
    11.    classDifficulty = Difficulty.class;
    12.         for (Field field : classDifficulty.getFields()) {
    13.                 instance.printFieldSignature(field);
    14.                 System.out.println();
    15.         }
    16.   }
    17.   /**
    18.    * 打印字段 签名(signature)
    19.    *
    20.    * @param field
    21.    */
    22.   private void printFieldSignature(Field field) {
    23.    StringBuilder message = new StringBuilder("字段  ")
    24.         .append(field.getName())
    25.         .append(" 的签名:")
    26.         .append(field.toString())
    27.         .append("        ");
    28.     System.out.print(message);
    29.    }
    30. }
    复制代码
    printFieldSignature 方法输出枚举 Difficulty的字段,其结果为:
    字段 NORMAL 的签名:public static final Difficulty Difficulty.NORMAL
    字段 MEDIUM 的签名:public static final Difficulty Difficulty.MEDIUM
    字段 HARD 的签名:public static final Difficulty Difficulty.HARD

    这个结果得出了两个结论,其一,每个枚举成员是枚举的字段。其二,每个成员都被 public static final。凡是 static final 变量都是 Java 中的“常量”,其保存在常量池中。根据其常量性和命名规则,建议全大写命名每个枚举的成员(前面提到)。 常量性提供了数据一致性,不必担心被被其他地方修改,同时保证了线程安全。因此在设计过程中,不必担心线程安全问题。 枚举类型是常量,那么在判断是可以使用== 符号来做比较。可是如果枚举成员能够克隆 (Clone) 的话 , 那么 == 比较会失效,从而一致性不能得到保证。如果按照类的定义方法,考虑枚举的话,那么枚举类是继承了 java.lang.Object 类,因此,它继承了 protected java.lang.Object clone() 方法,也就是说支持 clone ,虽然需要通过反射的手段去调用。 Java 语言规范提到,每个枚举继承了 java.lang.Enum<E> 抽象基类。 提供一段测试代码来验明真伪:
    1. /**
    2.          * 指定的类是枚举java.lang.Enum< E>的子类吗?
    3.          *
    4.          * @param klass
    5.          * @return
    6.          */
    7.         private boolean isEnum(Class< ?> klass) {
    8.                 Class< ?> superClass = klass.getSuperclass();
    9.                 while (true) { // 递归查找
    10.                         if (superClass != null) {
    11.                                 if (Enum.class.equals(superClass))
    12.                                         return true;
    13.                          } else {
    14.                                   break;
    15.                          }
    16.                         superClass = superClass.getSuperclass();
    17.                     }
    18.                 return false;
    19.           }

    复制代码

    客户端代码调用: instance.isEnum(Difficulty. class ) ; 结果返回true ,那么证明了 Difficulty 枚举继承了java.lang.Enum<E> 抽象基类。 那么java.lang.Enum<E> 有没有覆盖 clone 方法呢?查看一下源代码:
    Java代码
    1. /**
    2.      * Throws CloneNotSupportedException.  This guarantees that enums
    3.      * are never cloned, which is necessary to preserve their "singleton"
    4.      * status.
    5.      *
    6.      * @return (never returns)
    7.      */
    8.   protected final Object clone() throws CloneNotSupportedException{
    9.    throw new CloneNotSupportedException();
    10. }
    复制代码
    很明显,java.lang.Enum<E> 基类 final 定义了 clone 方法,即枚举不支持克隆,并且 Java doc 提到要保持单态性。那么这也是面向对象设计的原则之一 - 对于保持单态性的对象而言,尽可能不支持或者不暴露clone 方法。 三、 强类型 前面的两个特性,在前Java 5 时代,利用了常量字段也可以完成需要。比如可以这么设计Difficulty类的字段, public static final int NORMAL = 1;
    public static final int MEDIUM = 2;
    public static final int HARD = 3;  
    这么设计有一个缺点-弱类型,因为三个字段都是int原生型。比如有一个方法用于设置游戏难度,定义如下:
    public void setDifficulty(int difficulty)

    利用int类型作为参数,可能会有问题-如果参数在NORMAL,MEDIUM,HARD之外int数是可以接受的。利用规约的方法可以避免这个问题,比如设计范围检查:
    Java代码  /**
    * 设置游戏难度
    * @param difficulty 难度数
    * @throws IllegalArgumentException
    * 如果参数不是 NORMAL、MEDIUM和Hard其中一个,那么报出IllegalArgumentException
    */
    public void setDifficulty(int difficulty) throws IllegalArgumentException  在可能实现方法中,通过三次成员比较,这样比较笨拙和憋足。如果您在使用Java 5以前版本的话,
    作者提供一个较好的实现方法:
    1. package org.mercy.design.enumeration;
    2. import java.util.Collections;
    3. import java.util.HashSet;
    4. import java.util.Set;
    5. /**
    6. * Difficulty JDK1.4实现 Difficulty枚举
    7. * @author mercy
    8. */
    9. public class Difficulty14 {
    10.         //枚举字段
    11.         public static final int NORMAL = 1;
    12.         public static final int MEDIUM = 2;
    13.         public static final int HARD = 3;
    14.        
    15.         private final static Set difficulties;
    16.         static {
    17.                 // Hash 提供快速查找
    18.                 HashSet difficultySet = new HashSet();
    19.                 difficultySet.add(Integer.valueOf(NORMAL));
    20.                 difficultySet.add(Integer.valueOf(MEDIUM));
    21.                 difficultySet.add(Integer.valueOf(HARD));
    22.                 //利用不变的对象是一个好的设计实践
    23.                 difficulties= Collections.unmodifiableSet(difficultySet);
    24.         }
    25.        
    26.         /**
    27.          * 设置游戏难度
    28.          * @param difficulty 难度数
    29.          * @throws IllegalArgumentException
    30.          * 如果参数不是NORMAL、MEDIUM和Hard其中一个,那么报出IllegalArgumentException
    31.          */
    32.         public void setDifficulty(int difficulty)
    33.         throws IllegalArgumentException {
    34.                 if(!difficulties.contains(Integer.valueOf(difficulty)))
    35.                         throw new IllegalArgumentException("参数错误");
    36.                 //设置难度...
    37.         }
    38. }
    复制代码
       在上面的代码中,尽管提供了范围检查,不过参数范围还是巨大(可以说是无数),并且是运行时检查。因为 setDifficulty 的参数是 int的,客户端调用时候,编译器可以接受 int 以及范围更小的 short 、 byte 等类型。那么违反了 一个良好的实践 -在面向对象设计中,类型范围最好在编译时确定而非运行时。    另一个良好的面向对象实践 -利用对象类型,而不是原生型(如果编程语言支持的话)。 那么,如果使用java.lang.Integer 取代 int 类型,并且 Integer 是 final 类,没有必要担心多态的情况下,不就可以提供强类型吗?的确,提供了强类型约束,并且更好的锁定类型范围(因为是 final 的)。可是, Integer 的范围在一定程度上,认为是无限的,同时不支持 swtich 语句( 仅支持 int 、 short 、 byte 和 Java 5 枚举类型)。因此 Integer 还是不合适的。 枚举的常量性和可列性,在Difficulty 场景中尤其适合。 四、 类的特性      已知每个枚举都继承了java.lang.Enun< E> 基类,其既有常量性,同时也有类的特点。尽管它是一种被限制的类,比如name和ordinal字段状态都是有JVM处理的。不过开发人员可以充分的利用类的特点,作出优美的设计。 枚举既然也是类,那么也遵循类的设计。通过扩张 Difficulty 类,面向对象的方式来设计枚举。 1. 封装设计 如果有一个需求 - Difficulty 持久化,把其存入 d ifficult ies数据库表中,并且提供一个唯一的 id 整型值。面向对象的封装,枚举也适用。示例如下:
    1. public enum Difficulty {
    2.         // 注意:枚举成员命名,请使用英文大写形式
    3.         NORMAL(1), MEDIUM(2), HARD(3);
    4.         /**
    5.          * final修饰字段是一个良好的实践。
    6.          */
    7.         final private int id;
    8.        
    9.         Difficulty(final int id){
    10.                 this.id=id;
    11.         }
    12.         /**
    13.          * 获得ID
    14.          * @return
    15.          */
    16.         public int getId() {
    17.                 return id;
    18.         }
    19. }


    20. 通过调用getId 方法可以获取枚举成员的 ID 值。
    复制代码
    2. 抽象设计
         在不同的游戏难度级别中,不同任务的难度值不同(大多数情况是通过值来表示,而非枚举对象本身)。以Difficulty为例,定义一个抽象的方法,计算难度值。
    Java代码 /**
    * 获取不同任务的难度值
    *
    * @param mission
    * @return
    * @throws IllegalArgumentException
    * 如果<code>mission</code> 为负数时。
    */
    public abstract int getValue(int mission) throws IllegalArgumentException;

    3. 多态设计
         Difficulty 枚举中定义抽象方法getValue,那么其子类必须实现这个方法。不过枚举不能被继承,也不能继承其他类,除了默认的 java.lang.Enum<E>类以外。因此枚举是一个final的版本,不能实现抽象方法?     枚举的特殊在此,枚举虽然不能显示地利用extends关键字继承,不过它的每个成员都是自己的子类。那么以Difficulty为例,其类层次关系为:java.lang.Enum<E> -> Difficulty -> HARD。这么看来,每个枚举成员相当于定了一个 final的类内置类。 回到 Difficulty枚举,实现抽象方法如下:
    1. NORMAL(1) {
    2.   @Override
    3.   public int getValue(int mission) throws IllegalArgumentException {
    4.         return mission + this.getId();
    5.   }
    6. },
    7. MEDIUM(2) {
    8.   @Override
    9.   public int getValue(int mission) throws IllegalArgumentException {
    10.         return mission * this.getId();
    11.   }
    12. },
    13.        
    14. HARD(3) {
    15.   @Override
    16.   public int getValue(int mission) throws IllegalArgumentException {
    17.         return mission << this.getId();
    18.   }
    19. };
    复制代码
    4. 继承设计
         在上述实现中,可以观察到一点,每个实现getValue 的方法都利用的 getId() 方法。那么再次说明了枚举类本身也是一个特殊基类,可以定义模板方法。 5. 串行化设计
         在某些设计中,需要把枚举通过串行化。回到 Difficulty14 的示例中,三个成员变量都是常量,那么 static的变量是不可能被串行化的。如果去掉 static 修饰,那么语义将会被改变。而枚举不同,所有自定义枚举都是 java.lang.Enum<E> 的子类,因此所有的枚举都是可序列化的( java.lang.Enum<E> 实现了java.io.Serializable ) 。这也是枚举相对于常量的优势之一。 实现中可以提供类似这样的方法:
    Java代码
    private void readObject(ObjectInputStream in) throws IOException
    private void readObjectNoData() throws ObjectStreamException  
    总之,枚举也可以像类那样,实现面向对象的特点,不过值得一提的是, 枚举中应该保持尽量可能少的状态,职责单一的设计。 这篇文章是否给你带来了设计的灵感呢,笔者知识和经历有限,欢迎大家指正和相互学习。

      
      
       
       

         
       

         
       
      
    复制代码

    源码下载:http://file.javaxxz.com/2014/10/28/235556578.zip
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-26 04:45 , Processed in 0.303224 second(s), 36 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表