java 枚举

Java 枚举

概要

本篇博客的内容如下

  1. 使用背景
  2. 枚举定义和使用
  3. 枚举的总结
  4. 深度分析Java的枚举类型—-枚举的线程安全性及序列化问题

1. 使用背景

在java语言还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组int常量。之前通常利用public final static 方法定义。下面是一个小案例,分别用1 表示春天 2 表示夏天 3 表示秋天 4 表示冬天

public class Season {
   public static final int SPRING = 1;
   public static final int SUMMER = 2;
   public static final int AUTUMN = 3;
   public static final int WINTER = 4;
}

这种方式称作int枚举模式。但是这种模式有一些问题。通常我们写出的代码都会从以下三个方面来进行分析,安全性、易用性和可读性。首先我们考虑一下他的安全性。当然这种模式不是类型安全的。比如我们设计一个函数,要求传入春夏秋冬的某个值。但是使用int类型,我们无法保证传入的值为合法。代码如下所示

private String getChineseSeason(int season){
       StringBuffer result = new StringBuffer();
       switch(season){
           case Season.SPRING :
               result.append("春天");
               break;
           case Season.SUMMER :
               result.append("夏天");
               break;
           case Season.AUTUMN :
               result.append("秋天");
               break;
           case Season.WINTER :
               result.append("冬天");
               break;
           default :
               result.append("地球没有的季节");
               break;
       }
       return result.toString();
   }

   public void doSomething(){
       //这是正常的场景
       System.out.println(this.getChineseSeason(Season.SPRING));
       //这个却是不正常的场景,这就导致了类型不安全问题
       System.out.println(this.getChineseSeason(5));
   }

程序getChineseSeason(Season.SPRING)是我们预期的使用方法。可getChineseSeason(5)显然就不是了,而且编译很通过,在运行时会出现什么情况,我们就不得而知了。这显然就不符合Java程序的类型安全。

接下来我们考虑一下这种模式的可读性。使用枚举的大多数场合,我们都需要方便的到枚举类型的字符串表达式。如果将int枚举常量打印出来,我们所见到的就是一组数字,这是没什么太大的用处。我们可能会想到是一个String常量来代替int常量。虽然他为这些常量提供了可打印的字符串,但是他会导致性能问题,因为他依赖于字符串的比较操作,所以这种模式也是我们不期望的。从类型安全性和程序可读性俩方面考虑,int和String枚举模式的缺点就显露出来。幸运的是,从Java1.5发行版本开始,就提出了另一种可以替代的解决方案,可以避免intString枚举模式的缺点,并提供了许多额外的好处。那就是枚举类型(enum type)。接下来的章节将介绍枚举类型的定义、特征、应用场景和优缺点。

2. 枚举定义和使用

枚举类型(enum type)是指由一组固定的常量组成合法的类型。java中由关键字enum来定义一个枚举类型。下面就是java枚举类型的定义:

publi enum Season{
  SPRING, SUMMER, AUTUMN, WINER;
}

枚举的用法主要由以下几种:

  1. 常量,就如上面定义那样使用

  2. switch

    enum Signal {  
      GREEN, YELLOW, RED  
    }  
    public class TrafficLight {  
      Signal color = Signal.RED;  
      public void change() {  
          switch (color) {  
          case RED:  
              color = Signal.GREEN;  
              break;  
          case YELLOW:  
              color = Signal.RED;  
              break;  
          case GREEN:  
              color = Signal.YELLOW;  
              break;  
          }  
      }  
    }  
  3. 向枚举中添加新的方法

    public enum Color {  
      RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
      // 成员变量  
      private String name;  
      private int index;  
      // 构造方法  
      private Color(String name, int index) {  
          this.name = name;  
          this.index = index;  
      }  
      // 普通方法  
      public static String getName(int index) {  
          for (Color c : Color.values()) {  
              if (c.getIndex() == index) {  
                  return c.name;  
              }  
          }  
          return null;  
      }  
      // get set 方法  
      public String getName() {  
          return name;  
      }  
      public void setName(String name) {  
          this.name = name;  
      }  
      public int getIndex() {  
          return index;  
      }  
      public void setIndex(int index) {  
          this.index = index;  
      }  
    }  
  4. 覆盖枚举的方法

    public enum Color {  
      RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
      // 成员变量  
      private String name;  
      private int index;  
      // 构造方法  
      private Color(String name, int index) {  
          this.name = name;  
          this.index = index;  
      }  
      //覆盖方法  
      @Override  
      public String toString() {  
          return this.index+"_"+this.name;  
      }  
    }  
  5. 实现接口

    public interface Behaviour {  
      void print();  
      String getInfo();  
    }  
    public enum Color implements Behaviour{  
      RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
      // 成员变量  
      private String name;  
      private int index;  
      // 构造方法  
      private Color(String name, int index) {  
          this.name = name;  
          this.index = index;  
      }  
    //接口方法  
      @Override  
      public String getInfo() {  
          return this.name;  
      }  
      //接口方法  
      @Override  
      public void print() {  
          System.out.println(this.index+":"+this.name);  
      }  
    }  
  6. 使用接口组织枚举

    public interface Food {  
      enum Coffee implements Food{  
          BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO  
      }  
      enum Dessert implements Food{  
          FRUIT, CAKE, GELATO  
      }  
    }
  7. 常量相关的方法

    public enum Constantmethod{
    DATE_TIME{
      String getInfo(){
        System.out.println("date_time");
      }
    }
    CLASSPATH{
      String getInfo(){
        System.out.println("classpath");
      }
    }
    //定义每个常量都要实现的方法
    abstract String getInfo();
    }

基本使用

public enum Season {
   SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);

   private int code;
   private Season(int code){
       this.code = code;
   }

   public int getCode(){
       return code;
   }
}
public class UseSeason {
   /**
    * 将英文的季节转换成中文季节
    * @param season
    * @return
    */
   public String getChineseSeason(Season season){
       StringBuffer result = new StringBuffer();
       switch(season){
           case SPRING :
               result.append("[中文:春天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
               break;
           case AUTUMN :
               result.append("[中文:秋天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
               break;
           case SUMMER : 
               result.append("[中文:夏天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
               break;
           case WINTER :
               result.append("[中文:冬天,枚举常量:" + season.name() + ",数据:" + season.getCode() + "]");
               break;
           default :
               result.append("地球没有的季节 " + season.name());
               break;
       }
       return result.toString();
   }

   public void doSomething(){
       for(Season s : Season.values()){
           System.out.println(getChineseSeason(s));//这是正常的场景
       }
       //System.out.println(getChineseSeason(5));
       //此处已经是编译不通过了,这就保证了类型安全
   }

   public static void main(String[] arg){
       UseSeason useSeason = new UseSeason();
       useSeason.doSomething();
   }
}

3. 枚举的使用总结

其实枚举的定义很像类的定义,只不过枚举有一些限制,你甚至可以在枚举里面写main方法,这也是可以运行通过的。不过枚举不能继承类,至于原因接下来的博客会说明,但是枚举可以实现接口,就像上面展示的用法一样。

4.深度分析Java的枚举类型—-枚举的线程安全性及序列化问题

1. 枚举是如何保证线程安全

要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举:

public enum t {
   SPRING,SUMMER,AUTUMN,WINTER;
}

然后我们使用反编译,看看这段代码到底是怎么实现的,反编译(Java的反编译)后代码内容如下:

public final class T extends Enum
{
   private T(String s, int i)
   {
       super(s, i);
   }
   public static T[] values()
   {
       T at[];
       int i;
       T at1[];
       System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
       return at1;
   }

   public static T valueOf(String s)
   {
       return (T)Enum.valueOf(demo/T, s);
   }

   public static final T SPRING;
   public static final T SUMMER;
   public static final T AUTUMN;
   public static final T WINTER;
   private static final T ENUM$VALUES[];
   static
   {
       SPRING = new T("SPRING", 0);
       SUMMER = new T("SUMMER", 1);
       AUTUMN = new T("AUTUMN", 2);
       WINTER = new T("WINTER", 3);
       ENUM$VALUES = (new T[] {
           SPRING, SUMMER, AUTUMN, WINTER
       });
   }
}

通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,我们看到这个类中有几个属性和方法。

我们可以看到:

       public static final T SPRING;
       public static final T SUMMER;
       public static final T AUTUMN;
       public static final T WINTER;
       private static final T ENUM$VALUES[];
       static
       {
           SPRING = new T("SPRING", 0);
           SUMMER = new T("SUMMER", 1);
           AUTUMN = new T("AUTUMN", 2);
           WINTER = new T("WINTER", 3);
           ENUM$VALUES = (new T[] {
               SPRING, SUMMER, AUTUMN, WINTER
           });
       }

都是static类型的,因为static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的

为什么用枚举实现的单例是最好的方式

在[转+注]单例模式的七种写法中,我们看到一共有七种实现单例的方式,其中,Effective Java作者Josh Bloch 提倡使用枚举的方式,既然大神说这种方式好,那我们就要知道它为什么好?

1. 枚举写法简单

写法简单这个大家看看[转+注]单例模式的七种写法里面的实现就知道区别了。

public enum EasySingleton{
   INSTANCE;
}

你可以通过EasySingleton.INSTANCE来访问。

2. 枚举自己处理序列化

我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。原文如下:

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

大概意思就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf方法:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {  
           T result = enumType.enumConstantDirectory().get(name);  
           if (result != null)  
               return result;  
           if (name == null)  
               throw new NullPointerException("Name is null");  
           throw new IllegalArgumentException(  
               "No enum const " + enumType +"." + name);  
       }  

从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。

所以,JVM对序列化有保证。

3.枚举实例创建是thread-safe(线程安全的)

当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值