UDN-企业互联网技术人气社区

板块导航

浏览  : 1219
回复  : 0

[讨论交流] 关于Java中枚举Enum的深入剖析

[复制链接]
呵呵燕的头像 楼主
发表于 2016-11-30 09:45:27 | 显示全部楼层 |阅读模式
  在编程语言中我们,都会接触到枚举类型,通常我们进行有穷的列举来实现一些限定。Java也不例外。Java中的枚举类型为Enum,本文将对枚举进行一些比较深入的剖析。

  什么是Enum

  Enum是自Java 5 引入的特性,用来方便Java开发者实现枚举应用。一个简单的Enum使用如下。

  1.   // ColorEnum.java

  2.   public enum ColorEmun {

  3.   RED,

  4.   GREEN,

  5.   YELLOW

  6.   }

  7.   public void setColorEnum(ColorEmun colorEnum) {

  8.   //some code here

  9.   }

  10.   setColorEnum(ColorEmun.GREEN);
复制代码


  为什么会有Enum

  在Enum之前的我们使用类似如下的代码实现枚举的功能.

 
  1.  public static final int COLOR_RED = 0;

  2.   public static final int COLOR_GREEN = 1;

  3.   public static final int COLOR_YELLOW = 2;

  4.   public void setColor(int color) {

  5.   //some code here

  6.   }

  7.   //调用

  8.   setColor(COLOR_RED)
复制代码


  然而上面的还是有不尽完美的地方

  setColor(COLOR_RED)与setColor(0)效果一样,而后者可读性很差,但却可以正常运行

  setColor方法可以接受枚举之外的值,比如setColor(3),这种情况下程序可能出问题

  概括而言,传统枚举有如下两个弊端

  安全性

  可读性,尤其是打印日志时

  因此Java引入了Enum,使用Enum,我们实现上面的枚举就很简单了,而且还可以轻松避免传入非法值的风险.

  枚举原理是什么

  Java中Enum的本质其实是在编译时期转换成对应的类的形式。

  首先,为了探究枚举的原理,我们先简单定义一个枚举类,这里以季节为例,类名为Season,包含春夏秋冬四个枚举条目.

  1.   public enum Season {

  2.   SPRING,

  3.   SUMMER,

  4.   AUTUMN,

  5.   WINTER

  6.   }
复制代码


  然后我们使用javac编译上面的类,得到class文件.

 
  1.  javac Season.java
复制代码

  然后,我们利用反编译的方法来看看字节码文件究竟是什么.这里使用的工具是javap的简单命令,先列举一下这个Season下的全部元素.

  1.   ➜ company javap Season

  2.   Warning: Binary file Season contains com.company.Season

  3.   Compiled from "Season.java"

  4.   public final class com.company.Season extends java.lang.Enum {

  5.   public static final com.company.Season SPRING;

  6.   public static final com.company.Season SUMMER;

  7.   public static final com.company.Season AUTUMN;

  8.   public static final com.company.Season WINTER;

  9.   public static com.company.Season[] values();

  10.   public static com.company.Season valueOf(java.lang.String);

  11.   static {};

  12.   }
复制代码


  从上反编译结果可知

  java代码中的Season转换成了继承自的java.lang.enum的类

  既然隐式继承自java.lang.enum,也就意味java代码中,Season不能再继承其他的类

  Season被标记成了final,意味着它不能被继承

  static代码块

  使用javap具体反编译class文件,得到静态代码块相关的结果为

 
  1.  static {};

  2.   Code:

  3.   0: new #4 // class com/company/Season

  4.   3: dup

  5.   4: ldc #7 // String SPRING

  6.   6: iconst_0

  7.   7: invokespecial #8 // Method "":(Ljava/lang/String;I)V

  8.   10: putstatic #9 // Field SPRING:Lcom/company/Season;

  9.   13: new #4 // class com/company/Season

  10.   16: dup

  11.   17: ldc #10 // String SUMMER

  12.   19: iconst_1

  13.   20: invokespecial #8 // Method "":(Ljava/lang/String;I)V

  14.   23: putstatic #11 // Field SUMMER:Lcom/company/Season;

  15.   26: new #4 // class com/company/Season

  16.   29: dup

  17.   30: ldc #12 // String AUTUMN

  18.   32: iconst_2

  19.   33: invokespecial #8 // Method "":(Ljava/lang/String;I)V

  20.   36: putstatic #13 // Field AUTUMN:Lcom/company/Season;

  21.   39: new #4 // class com/company/Season

  22.   42: dup

  23.   43: ldc #14 // String WINTER

  24.   45: iconst_3

  25.   46: invokespecial #8 // Method "":(Ljava/lang/String;I)V

  26.   49: putstatic #15 // Field WINTER:Lcom/company/Season;

  27.   52: iconst_4

  28.   53: anewarray #4 // class com/company/Season

  29.   56: dup

  30.   57: iconst_0

  31.   58: getstatic #9 // Field SPRING:Lcom/company/Season;

  32.   61: aastore

  33.   62: dup

  34.   63: iconst_1

  35.   64: getstatic #11 // Field SUMMER:Lcom/company/Season;

  36.   67: aastore

  37.   68: dup

  38.   69: iconst_2

  39.   70: getstatic #13 // Field AUTUMN:Lcom/company/Season;

  40.   73: aastore

  41.   74: dup

  42.   75: iconst_3

  43.   76: getstatic #15 // Field WINTER:Lcom/company/Season;

  44.   79: aastore

  45.   80: putstatic #1 // Field $VALUES:[Lcom/company/Season;

  46.   83: return

  47.   }
复制代码


  其中

  0~52为实例化SPRING, SUMMER, AUTUMN, WINTER

  53~83为创建Season[]数组$VALUES,并将上面的四个对象放入数组的操作.

  values方法

  values方法的的返回值实际上就是上面$VALUES数组对象

  swtich中的枚举

  在Java中,switch-case是我们经常使用的流程控制语句.当枚举出来之后,switch-case也很好的进行了支持.

  比如下面的代码是完全正常编译,正常运行的.

  
  1. public static void main(String[] args) {

  2.   Season season = Season.SPRING;

  3.   switch(season) {

  4.   case SPRING:

  5.   System.out.println("It's Spring");

  6.   break;

  7.   case WINTER:

  8.   System.out.println("It's Winter");

  9.   break;

  10.   case SUMMER:

  11.   System.out.println("It's Summer");

  12.   break;

  13.   case AUTUMN:

  14.   System.out.println("It's Autumn");

  15.   break;

  16.   }

  17.   }
复制代码


  不过,通常情况下switch-case支持类似int的类型,那么它是怎么做到对Enum的支持呢,我们不放反编译上述方法看一下字节码的真实情况.

  1.   public static void main(java.lang.String[]);

  2.   Code:

  3.   0: getstatic #2 // Field com/company/Season.SPRING:Lcom/company/Season;

  4.   3: astore_1

  5.   4: getstatic #3 // Field com/company/Main$1.$SwitchMap$com$company$Season:[I

  6.   7: aload_1

  7.   8: invokevirtual #4 // Method com/company/Season.ordinal:()I

  8.   11: iaload

  9.   12: tableswitch { // 1 to 4

  10.   1: 44

  11.   2: 55

  12.   3: 66

  13.   4: 77

  14.   default: 85

  15.   }

  16.   44: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;

  17.   47: ldc #6 // String It's Spring

  18.   49: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

  19.   52: goto 85

  20.   55: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;

  21.   58: ldc #8 // String It's Winter

  22.   60: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

  23.   63: goto 85

  24.   66: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;

  25.   69: ldc #9 // String It's Summer

  26.   71: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

  27.   74: goto 85

  28.   77: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;

  29.   80: ldc #10 // String It's Autumn

  30.   82: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

  31.   88: return
复制代码

  注意上面代码块有这样的一段代码

 
  1.  8: invokevirtual #4 // Method com/company/Season.ordinal:()I
复制代码


  事实果真如此,在switch-case中,还是将Enum转成了int值(通过调用Enum.oridinal()方法)

  枚举与混淆

  在Android开发中,进行混淆是我们在发布前必不可少的工作,混下后,我们能增强反编译的难度,在一定程度上保护了增强了安全性.

  而开发人员处理混淆更多的是将某些元素加入不混淆的名单,这里枚举就是需要排除混淆的.

  在默认的混淆配置文件中,已经加入了关于对枚举混淆的处理

 
  1.  # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations

  2.   -keepclassmembers enum * {

  3.   public static **[] values();

  4.   public static ** valueOf(java.lang.String);

  5.   }
复制代码


  关于为什么要保留values()方法和valueOf()方法,请参考文章读懂 Android 中的代码混淆 关于枚举的部分

  使用proguard优化

  使用Proguard进行优化,可以将枚举尽可能的转换成int。配置如下

 
  1.  -optimizations class/unboxing/enum
复制代码


  确保上述代码生效,需要确proguard配置文件不包含-dontoptimize指令。

  当我们使用gradlew打包是,看到类似下面的输出,即Number of unboxed enum classes:1代表已经将一个枚举转换成了int的形式。


  1.   Optimizing...

  2.   Number of finalized classes: 0 (disabled)

  3.   Number of unboxed enum classes: 1

  4.   Number of vertically merged classes: 0 (disabled)

  5.   Number of horizontally merged classes: 0 (disabled)
复制代码


  枚举单例

  单例模式是我们在日常开发中可谓是最常用的设计模式.

  然后要设计好单例模式,无非考虑一下几点

  确保只有唯一实例,不多创建多余实例

  确保实例按需创建.

  因此传统的做法想要实现单例,大致有一下几种

  饿汉式加载

  懒汉式synchronize和双重检查

  利用java的静态加载机制

  相比上述的方法,使用枚举也可以实现单例,而且还更加简单.

 
  1.  public enum AppManager {

  2.   INSTANCE;

  3.   private String tagName;

  4.   public void setTag(String tagName) {

  5.   this.tagName = tagName;

  6.   }

  7.   public String getTag() {

  8.   return tagName;

  9.   }

  10.   }
复制代码


  调用起来也更加简单

  
  1. AppManager.INSTANCE.getTag();
复制代码

  枚举如何确保唯一实例

  因为获得实例只能通过AppManager.INSTANCE下面的方式是不可以的

  1.   AppManager appManager = new AppManager(); //compile error
复制代码

  关于单例模式,可以阅读单例这种设计模式了解更多。

  (Android中)该不该用枚举

  既然上面提到了枚举会转换成类,这样理论上造成了下面的问题

  增加了dex包的大小,理论上dex包越大,加载速度越慢

  同时使用枚举,运行时的内存占用也会相对变大

  关于上面两点的验证,秋百万已经做了详细的论证,大家可以参考这篇文章《Android 中的 Enum 到底占多少内存?该如何用?》

  关于枚举是否使用的结论,大家可以参考

  如果你开发的是Framework不建议使用enum

  如果是简单的enum,可以使用int很轻松代替,则不建议使用enum

  另外,如果是Android中,可以使用下面介绍的枚举注解来实现。

  除此之外,我们还需要对比可读性和易维护性来与性能进行衡量,从中进行做出折中

  在Android中的替代

  Android中新引入的替代枚举的注解有IntDef和StringDef,这里以IntDef做例子说明一下.

  
  1. public class Colors {

  2.   @IntDef({RED, GREEN, YELLOW})

  3.   @Retention(RetentionPolicy.SOURCE)

  4.   public @interface LightColors{}

  5.   public static final int RED = 0;

  6.   public static final int GREEN = 1;

  7.   public static final int YELLOW = 2;

  8.   }
复制代码


  声明必要的int常量

  声明一个注解为LightColors

  使用@IntDef修饰LightColors,参数设置为待枚举的集合

  使用@Retention(RetentionPolicy.SOURCE)指定注解仅存在与源码中,不加入到class文件中

  比如我们用来标注方法的参数

  1.   private void setColor(@Colors.LightColors int color) {

  2.   Log.d("MainActivity", "setColor color=" + color);

  3.   }
复制代码


  调用的该方法的时候

 
  1.  setColor(Colors.GREEN);
复制代码


  关于Android中的枚举,可以参考探究Android中的注解

  以上就是我对Java中enum的一些深入的剖析,欢迎大家不吝赐教。

原文作者:佚名  来源:开发者头条

相关帖子

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

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部