关于Java中枚举Enum的深入剖析

yangtz584 7年前
   <p>在编程语言中我们,都会接触到枚举类型,通常我们进行有穷的列举来实现一些限定。Java也不例外。Java中的枚举类型为Enum,本文将对枚举进行一些比较深入的剖析。</p>    <h2><strong>什么是Enum</strong></h2>    <p>Enum是自Java 5 引入的特性,用来方便Java开发者实现枚举应用。一个简单的Enum使用如下。</p>    <pre>  <code class="language-java">// ColorEnum.java  public enum ColorEmun {      RED,      GREEN,      YELLOW  }    public void setColorEnum(ColorEmun colorEnum) {      //some code here  }    setColorEnum(ColorEmun.GREEN);</code></pre>    <h2><strong>为什么会有Enum</strong></h2>    <p>在Enum之前的我们使用类似如下的代码实现枚举的功能.</p>    <pre>  <code class="language-java">public static final int COLOR_RED = 0;  public static final int COLOR_GREEN = 1;  public static final int COLOR_YELLOW = 2;    public void setColor(int color) {      //some code here  }  //调用  setColor(COLOR_RED)</code></pre>    <p>然而上面的还是有不尽完美的地方</p>    <ul>     <li>setColor(COLOR_RED)与setColor(0)效果一样,而后者可读性很差,但却可以正常运行</li>     <li>setColor方法可以接受枚举之外的值,比如setColor(3),这种情况下程序可能出问题</li>    </ul>    <p>概括而言,传统枚举有如下两个弊端</p>    <ul>     <li>安全性</li>     <li>可读性,尤其是打印日志时</li>    </ul>    <p>因此Java引入了Enum,使用Enum,我们实现上面的枚举就很简单了,而且还可以轻松避免传入非法值的风险.</p>    <h2><strong>枚举原理是什么</strong></h2>    <p>Java中Enum的本质其实是在编译时期转换成对应的类的形式。</p>    <p>首先,为了探究枚举的原理,我们先简单定义一个枚举类,这里以季节为例,类名为Season,包含春夏秋冬四个枚举条目.</p>    <pre>  <code class="language-java">public enum Season {      SPRING,      SUMMER,      AUTUMN,      WINTER  }</code></pre>    <p>然后我们使用javac编译上面的类,得到class文件.</p>    <pre>  <code class="language-java">javac Season.java</code></pre>    <p>然后,我们利用反编译的方法来看看字节码文件究竟是什么.这里使用的工具是javap的简单命令,先列举一下这个Season下的全部元素.</p>    <pre>  <code class="language-java">➜  company javap Season  Warning: Binary file Season contains com.company.Season  Compiled from "Season.java"  public final class com.company.Season extends java.lang.Enum<com.company.Season> {    public static final com.company.Season SPRING;    public static final com.company.Season SUMMER;    public static final com.company.Season AUTUMN;    public static final com.company.Season WINTER;    public static com.company.Season[] values();    public static com.company.Season valueOf(java.lang.String);    static {};  }</code></pre>    <p>从上反编译结果可知</p>    <ul>     <li>java代码中的Season转换成了继承自的java.lang.enum的类</li>     <li>既然隐式继承自java.lang.enum,也就意味java代码中,Season不能再继承其他的类</li>     <li>Season被标记成了final,意味着它不能被继承</li>    </ul>    <h3><strong>static代码块</strong></h3>    <p>使用javap具体反编译class文件,得到静态代码块相关的结果为</p>    <pre>  <code class="language-java">static {};      Code:         0: new           #4                  // class com/company/Season         3: dup         4: ldc           #7                  // String SPRING         6: iconst_0         7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V        10: putstatic     #9                  // Field SPRING:Lcom/company/Season;        13: new           #4                  // class com/company/Season        16: dup        17: ldc           #10                 // String SUMMER        19: iconst_1        20: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V        23: putstatic     #11                 // Field SUMMER:Lcom/company/Season;        26: new           #4                  // class com/company/Season        29: dup        30: ldc           #12                 // String AUTUMN        32: iconst_2        33: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V        36: putstatic     #13                 // Field AUTUMN:Lcom/company/Season;        39: new           #4                  // class com/company/Season        42: dup        43: ldc           #14                 // String WINTER        45: iconst_3        46: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V        49: putstatic     #15                 // Field WINTER:Lcom/company/Season;        52: iconst_4        53: anewarray     #4                  // class com/company/Season        56: dup        57: iconst_0        58: getstatic     #9                  // Field SPRING:Lcom/company/Season;        61: aastore        62: dup        63: iconst_1        64: getstatic     #11                 // Field SUMMER:Lcom/company/Season;        67: aastore        68: dup        69: iconst_2        70: getstatic     #13                 // Field AUTUMN:Lcom/company/Season;        73: aastore        74: dup        75: iconst_3        76: getstatic     #15                 // Field WINTER:Lcom/company/Season;        79: aastore        80: putstatic     #1                  // Field $VALUES:[Lcom/company/Season;        83: return  }</code></pre>    <p>其中</p>    <ul>     <li>0~52为实例化SPRING, SUMMER, AUTUMN, WINTER</li>     <li>53~83为创建Season[]数组$VALUES,并将上面的四个对象放入数组的操作.</li>    </ul>    <h3>values方法</h3>    <p>values方法的的返回值实际上就是上面$VALUES数组对象</p>    <h2><strong>swtich中的枚举</strong></h2>    <p>在Java中,switch-case是我们经常使用的流程控制语句.当枚举出来之后,switch-case也很好的进行了支持.</p>    <p>比如下面的代码是完全正常编译,正常运行的.</p>    <pre>  <code class="language-java">public static void main(String[] args) {          Season season = Season.SPRING;          switch(season) {              case SPRING:                  System.out.println("It's Spring");                  break;                case WINTER:                  System.out.println("It's Winter");                  break;                case SUMMER:                  System.out.println("It's Summer");                  break;              case AUTUMN:                  System.out.println("It's Autumn");                  break;          }      }</code></pre>    <p>不过,通常情况下switch-case支持类似int的类型,那么它是怎么做到对Enum的支持呢,我们不放反编译上述方法看一下字节码的真实情况.</p>    <pre>  <code class="language-java">public static void main(java.lang.String[]);      Code:         0: getstatic     #2                  // Field com/company/Season.SPRING:Lcom/company/Season;         3: astore_1         4: getstatic     #3                  // Field com/company/Main$1.$SwitchMap$com$company$Season:[I         7: aload_1         8: invokevirtual #4                  // Method com/company/Season.ordinal:()I        11: iaload        12: tableswitch   { // 1 to 4                       1: 44                       2: 55                       3: 66                       4: 77                 default: 85            }        44: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;        47: ldc           #6                  // String It's Spring        49: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V        52: goto          85        55: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;        58: ldc           #8                  // String It's Winter        60: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V        63: goto          85        66: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;        69: ldc           #9                  // String It's Summer        71: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V        74: goto          85        77: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;        80: ldc           #10                 // String It's Autumn        82: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V        88: return</code></pre>    <p>注意上面代码块有这样的一段代码</p>    <pre>  <code class="language-java">8: invokevirtual #4                  // Method com/company/Season.ordinal:()I</code></pre>    <p>事实果真如此,在switch-case中,还是将Enum转成了int值(通过调用Enum.oridinal()方法)</p>    <h2><strong>枚举与混淆</strong></h2>    <p>在Android开发中,进行混淆是我们在发布前必不可少的工作,混下后,我们能增强反编译的难度,在一定程度上保护了增强了安全性.</p>    <p>而开发人员处理混淆更多的是将某些元素加入不混淆的名单,这里枚举就是需要排除混淆的.</p>    <p>在默认的混淆配置文件中,已经加入了关于对枚举混淆的处理</p>    <pre>  <code class="language-java"># For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations  -keepclassmembers enum * {      public static **[] values();      public static ** valueOf(java.lang.String);  }</code></pre>    <h2><strong>使用proguard优化</strong></h2>    <p>使用Proguard进行优化,可以将枚举尽可能的转换成int。配置如下</p>    <pre>  <code class="language-java">-optimizations class/unboxing/enum</code></pre>    <p>确保上述代码生效,需要确proguard配置文件不包含-dontoptimize指令。</p>    <p>当我们使用gradlew打包是,看到类似下面的输出,即Number of unboxed enum classes:1代表已经将一个枚举转换成了int的形式。</p>    <pre>  <code class="language-java">Optimizing...    Number of finalized classes:                 0   (disabled)    Number of unboxed enum classes:              1    Number of vertically merged classes:         0   (disabled)    Number of horizontally merged classes:       0   (disabled)</code></pre>    <h2><strong>枚举单例</strong></h2>    <p>单例模式是我们在日常开发中可谓是最常用的设计模式.</p>    <p>然后要设计好单例模式,无非考虑一下几点</p>    <ul>     <li>确保只有唯一实例,不多创建多余实例</li>     <li>确保实例按需创建.</li>    </ul>    <p>因此传统的做法想要实现单例,大致有一下几种</p>    <ul>     <li>饿汉式加载</li>     <li>懒汉式synchronize和双重检查</li>     <li>利用java的静态加载机制</li>    </ul>    <p>相比上述的方法,使用枚举也可以实现单例,而且还更加简单.</p>    <pre>  <code class="language-java">public enum AppManager {      INSTANCE;        private String tagName;      public void setTag(String tagName) {          this.tagName = tagName;      }        public String getTag() {          return tagName;      }  }</code></pre>    <p>调用起来也更加简单</p>    <pre>  <code class="language-java">AppManager.INSTANCE.getTag();</code></pre>    <h3><strong>枚举如何确保唯一实例</strong></h3>    <p>因为获得实例只能通过AppManager.INSTANCE</p>    <p>下面的方式是不可以的</p>    <pre>  <code class="language-java">AppManager appManager = new AppManager(); //compile error</code></pre>    <h2><strong>(Android中)该不该用枚举</strong></h2>    <p>既然上面提到了枚举会转换成类,这样理论上造成了下面的问题</p>    <ul>     <li>增加了dex包的大小,理论上dex包越大,加载速度越慢</li>     <li>同时使用枚举,运行时的内存占用也会相对变大</li>    </ul>    <p>关于枚举是否使用的结论,大家可以参考</p>    <ul>     <li>如果你开发的是Framework不建议使用enum</li>     <li>如果是简单的enum,可以使用int很轻松代替,则不建议使用enum</li>     <li>另外,如果是Android中,可以使用下面介绍的枚举注解来实现。</li>     <li>除此之外,我们还需要对比可读性和易维护性来与性能进行衡量,从中进行做出折中</li>    </ul>    <h2><strong>在Android中的替代</strong></h2>    <p>Android中新引入的替代枚举的注解有IntDef和StringDef,这里以IntDef做例子说明一下.</p>    <pre>  <code class="language-java">public class Colors {      @IntDef({RED, GREEN, YELLOW})      @Retention(RetentionPolicy.SOURCE)      public @interface LightColors{}        public static final int RED = 0;      public static final int GREEN = 1;      public static final int YELLOW = 2;  }</code></pre>    <ul>     <li>声明必要的int常量</li>     <li>声明一个注解为LightColors</li>     <li>使用@IntDef修饰LightColors,参数设置为待枚举的集合</li>     <li>使用@Retention(RetentionPolicy.SOURCE)指定注解仅存在与源码中,不加入到class文件中</li>    </ul>    <p>比如我们用来标注方法的参数</p>    <pre>  <code class="language-java">private void setColor(@Colors.LightColors int color) {          Log.d("MainActivity", "setColor color=" + color);  }</code></pre>    <p>调用的该方法的时候</p>    <pre>  <code class="language-java">setColor(Colors.GREEN);</code></pre>    <p> </p>    <p> </p>    <p>来自:http://www.udpwork.com/item/15951.html</p>    <p> </p>