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

在编程语言中我们,都会接触到枚举类型,通常我们进行有穷的列举来实现一些限定。Java也不例外。Java中的枚举类型为Enum,本文将对枚举进行一些比较深入的剖析。

什么是Enum

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

// ColorEnum.java

public enum ColorEmun {

    RED,

    GREEN,

    YELLOW

}

public void setColorEnum(ColorEmun colorEnum) {

    //some code here

}

setColorEnum(ColorEmun.GREEN);

为什么会有Enum

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

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)

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

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

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

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

安全性

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

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

枚举原理是什么

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

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

public enum Season {

    SPRING,

    SUMMER,

    AUTUMN,

    WINTER

}

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

javac Season.java

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

➜  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 {};

}

从上反编译结果可知

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

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

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

static代码块

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

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

}

其中

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

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

values方法

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

swtich中的枚举

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

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

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;

        }

    }

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

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

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

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

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

枚举与混淆

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

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

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

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

-keepclassmembers enum * {

    public static **[] values();

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

}

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

使用proguard优化

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

-optimizations class/unboxing/enum

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

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

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)

枚举单例

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

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

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

确保实例按需创建.

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

饿汉式加载

懒汉式synchronize和双重检查

利用java的静态加载机制

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

public enum AppManager {

    INSTANCE;

    private String tagName;

    public void setTag(String tagName) {

        this.tagName = tagName;

    }

    public String getTag() {

        return tagName;

    }

}

调用起来也更加简单

AppManager.INSTANCE.getTag();

枚举如何确保唯一实例

因为获得实例只能通过AppManager.INSTANCE

下面的方式是不可以的

AppManager appManager = new AppManager(); //compile error

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

(Android中)该不该用枚举

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

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

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

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

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

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

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

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

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

在Android中的替代

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

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;

}

声明必要的int常量

声明一个注解为LightColors

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

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

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

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

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

}

调用的该方法的时候

setColor(Colors.GREEN);

Java高架构师、分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师视频免费获取架构群:854180697    加群链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容