本章知识点

  • Enume 枚举类型的重要方法的使用
  • 自定义枚举变量并为自定义枚举类添加相关方法
  • 分类枚举的组织方式
  • EnumeSet 和 EnumeMap的使用

在开始介绍今天的专题之前我们首先需要明确下什么时候我们会使用到枚举,在未接触枚举之前,大家或许写过这样的代码:

public static final int LIGHT_STATUS_OFF = 1; //表示灯关闭的状态
public static final int LIGHT_STATUS_ON = 2; //表示灯开启的状态

也就是用一些常量表示某个时刻的状态,这种表示方式有如下几个缺点:

  • 类型不安全
    由于状态的对应值是整型,所以程序执行过程中很有可能传入一个任意的整数值,从而导致出现错误。
  • 一致性差
    因为整型枚举属于编译期常量,所以编译过程完成后,所有客户端和服务器端引用的地方,会直接将整数值写入。这样,当你修改旧的枚举整数值后或者增加新的枚举值后,所有引用地方代码都需要重新编译,否则运行时刻就会出现错误。
  • 可读性差
    由于上述用于枚举的仅仅是一些无任何含义的整数值,如果在运行期调试时候,你就会发现日志中有很多难以理解的数字,但除了程序员本身,其他人很难明白其所表示的真实含义,比较不直观。

为了解决这些问题在5.0 版本 SDK 发布时候引入了枚举特性,通过枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。

上面的常量可以通过下面的枚举变量进行实现:

public enume LIGHT_STATUS {
LIGHT_STATUS_OFF,
LIGHT_STATUS_ON
}

Enume 枚举类型的重要方法的使用

Enume 对象的可用方法不多,列举如下:

values() 用于返回整个枚举所包含的状态值
name() 用于返回某个enume item的名字
ordinal() 用于获取当前enume item 在枚举类中声明时候的次序
getDeclaringClass() 用于获取当前enume item在哪个枚举类中进行声明
private enum LIGHT_STATUS {
LIGHT_STATUS_ON,
LIGHT_STATUS_OFF;
}

public void enumBaseUsage() {
System.out.println("There are "+LIGHT_STATUS.values().length+" members in LIGHT_STATUS");
for(LIGHT_STATUS status:LIGHT_STATUS.values()) {
System.out.println(status.ordinal()+" : "+status.name()+ " : Declaring in "+status.getDeclaringClass().getSimpleName());
}
}

由于编译器会自动为我们提供equals()以及hashCode(),所以可以使用==来比较enume实例,同时由于它也实现了Comparable接口所以也可以使用compareTo方法进行比较,除了实现了Comparable 接口外,它还实现了Serializable接口。

public void enumeValueCompare() {

LIGHT_STATUS statusA = LIGHT_STATUS.LIGHT_STATUS_ON;
LIGHT_STATUS statusB = LIGHT_STATUS.LIGHT_STATUS_ON;
LIGHT_STATUS statusC = LIGHT_STATUS.LIGHT_STATUS_OFF;
if(statusA == statusB) {
System.out.println("equals");
} else {
System.out.println("not equals");
}

if(statusA == statusC) {
System.out.println("equals");
} else {
System.out.println("not equals");
}

if(statusA.compareTo(statusC)>0) {
System.out.println(" >0 ");
} else if(statusA.compareTo(statusC) == 0){
System.out.println(" =0");
} else {
System.out.println(" <0");
}
}

自定义枚举变量并为自定义枚举类添加相关方法

枚举的定义和类的定义十分相似,但是也存在着一些区别,下面是二者的区别点:

  • 首先枚举类型不能使用extends进行继承
  • 枚举的定义分成两个部分,第一部分用于定义枚举实例,第二部分用于定义属性以及方法,但是需要注意点是在定义enume实例之前不能定义先定义任何属性活着方法,
  • 对于枚举类的构造方法,一旦enume定义结束,编译器就不允许我们再使用其构造方法来创建任何实例。

下面是自定义的一个枚举类型:

private enum ITEM_TYPE {
ITEM_TYPE_NORMAL("The item which is used to show normal item"){
@Override
public void getInfo() {
System.out.println("ITEM_TYPE_NORMAL");
}
},
ITEM_TYPE_NEWEST("The item which is used to show newest item"){
@Override
public void getInfo() {
System.out.println("ITEM_TYPE_NEWEST");
}
},
ITEM_TYPE_HOTEST("The item which is used to show hotest item"){
@Override
public void getInfo() {
System.out.println("ITEM_TYPE_HOTEST");
}
};
private String description;
public abstract void getInfo();
ITEM_TYPE(String description) {
this.description = description;
}

public String getDescription() {
return description;
}
}

它包括每个枚举类型实例,以及枚举类型的描述,下面是测试例子:
上面的例子中我们通过声明抽象方法,在每个enume 实例中进行覆写,来指定每个enume实例相关的方法,而在实例声明之后的方法实现的是全部实例共有的方法。

public void customEnume() {
for (ITEM_TYPE itemType : ITEM_TYPE.values()) {
System.out.println(itemType.name() +" : " + itemType.getDescription() );
}
}

我们看sdk源码的时候可以发现enume实际上是继承自Enume类的,但是Enume 类中并没有values方法,实际上values是编译器添加的静态方法,所以如果我们将enume实例向上转型为Enum  那么 values方法就不可使用了。
解决这个问题我们可以借助Class类中的getEnumeConstants 方法来获取所有enume实例。

public void getEnumeConstants() {
Enum<ITEM_TYPE> itemTypeEnum = ITEM_TYPE.ITEM_TYPE_NEWEST;;
for (Enum item : itemTypeEnum.getClass().getEnumConstants()) {
System.out.println(item.name());
}
}

分类枚举的组织方式

有时候我们每个枚举实例中还包含其他分类,这时候就要讲究枚举类的分类了:
下面是一种比较标准的分类写法,它使用接口Food将不同的子类组织起来,再通过getEnumConstants
获取每种子类型的值:

private enum FOOD {
COFFEE(Food.Coffee.class),DESSERT(Food.Dessert.class);
private interface Food {
enum Coffee implements Food {
BLACK_COFFEE,WHITE_COFFEE,TEA;
}
enum Dessert implements Food {
FRUIT,GELATO;
}
}

Food[] ItemValues;
FOOD(Class<? extends Food> cls) {
ItemValues = cls.getEnumConstants();
}

public Food[] getItemValues() {
return ItemValues;
}
}

EnumeSet 和 EnumeMap的使用

EnumeSet用法:
在刚接触枚举的时候,当时没有觉得它有什么用处,第一感觉它和枚举没啥区别。但是后面才发现二者的区别,传统的enume不能添加或者删除元素,但是EnumeSet 可以添加删除。
假设我们有一种情况下,某个版本有不同的特性,这些特性分别用枚举实例来表示。那么这时候我们就可以构造一个具有全部特性的EnumeSet  再根据版本号来将代表某写特性的枚举值添加或者移除:

public void testEnumeSet() {
EnumSet<ITEM_TYPE> itemTypes = EnumSet.allOf(ITEM_TYPE.class);
System.out.println(itemTypes);
itemTypes = EnumSet.noneOf(ITEM_TYPE.class);
System.out.println(itemTypes);
itemTypes = EnumSet.of(ITEM_TYPE.ITEM_TYPE_HOTEST);
System.out.println(itemTypes);
itemTypes = EnumSet.range(ITEM_TYPE.ITEM_TYPE_NEWEST, ITEM_TYPE.ITEM_TYPE_HOTEST);
System.out.println(itemTypes);
}

EnumeMap用法:

public void testEnumeMap() {
EnumMap<LIGHT_STATUS, command> itemMap = new EnumMap<LIGHT_STATUS, command>(LIGHT_STATUS.class);
itemMap.put(LIGHT_STATUS.LIGHT_STATUS_ON, new command() {
@Override
public void action() {
System.out.println("The light is on, i will turn off the light");
}
});

itemMap.put(LIGHT_STATUS.LIGHT_STATUS_OFF, new command() {
@Override
public void action() {
System.out.println("The light is off, i will turn on the light");
}
});
itemMap.get(LIGHT_STATUS.LIGHT_STATUS_ON).action();
itemMap.get(LIGHT_STATUS.LIGHT_STATUS_OFF).action();
}

如何避免错误使用 Enum

在使用 Enum 时候有几个地方需要注意:
enum 类型不支持 public 和 protected 修饰符的构造方法,因此构造函数一定要是 private。也正因为如此,所以枚举对象是无法在程序中通过直接调用其构造方法来初始化的。
定义 enum 类型时候,如果是简单类型,那么最后一个枚举值后不用跟任何一个符号;但如果有定制方法,那么最后一个枚举值与后面代码要用分号’;’隔开,不能用逗号或空格。
由于 enum 类型的值实际上是通过运行期构造出对象来表示的,所以在 cluster 环境下,每个虚拟机都会构造出一个同义的枚举对象。因而在做比较操作时候就需要注意,如果直接通过使用等号 ( ‘ == ’ ) 操作符,这些看似一样的枚举值一定不相等,因为这不是同一个对象实例。
一个 Java 源文件中最多只能有一个 public 类型的枚举类,且该 Java 源文件的名字也必须和该枚举类的类名相同
使用 enum 定义的枚举类默认继承了 java.lang.Enum 类,并实现了 java.lang.Seriablizable 和 java.lang.Comparable 两个接口;
枚举类的所有实例(枚举值)必须在枚举类的第一行显式地列出,否则这个枚举类将永远不能产生实例。列出这些实例(枚举值)时,系统会自动添加 public static final 修饰,无需显式添加。

在Android 开发中为什么需要节制使用枚举

(该部分引用自 http://www.codeceo.com/article/why-android-not-use-enums.html)
关于Android性能优化中一个常见的建议是不要在你的代码中使用Enums,就连 Android官网上都强烈建议不要使用。为什么?
因为使用枚举之后增加了dex包的大小,理论上dex包越大,加载速度越慢
同时使用枚举,运行时的内存占用也会相对变大 (Enums often require more than twice as much memory as static constants)

Android中当你的App启动后系统会给App单独分配一块内存。App的DEX code、Heap以及运行时的内存分配都会在这块内存中。接下来看两种写法:

  • 使用Int表示状态
    public static final int VALUE1 =1;
    public static final int VALUE1 =2;
    public static final int VALUE1 =3;
  • 使用Enums表示状态
    public static enum Value{
    VALUE1,
    VALUE2,
    VALUE3
    }
    情形2中的DEX size增加是情形1中的13倍之多。这还只是DEX code的增加,同样,运行时的内存分配,一个enum值的声明会消耗至少20 bytes,这还不算其中的对象数组需要保持对enum值的引用。Why?使用javap反编译情形二中生成的class文件,去掉汇编代码后如下:
    public final class VALUE extends java.lang.Enum{  
    public static final VALUE VALUE1;
    public static final VALUE VALUE2;
    public static final VALUE VALUE3;
    private static final VALUE[] values[];
    static{}
    }
    可以看到实际上enum类型继承java.lang.Enum,每个枚举项都会被声明成一个静态变量,并被赋值。VALUE value1 = VALUE.VALUE1则会引起对静态变量的引用。
    因此,当你的代码或包含的Lib中大量使用enums时,对于本身内存小的手机将是灾难性的。不可否认enums会使得代码更易读更安全,那么我们需要如何在二者之间做权衡呢:
    我们可以看这个帖子的回答:

http://stackoverflow.com/questions/29183904/should-i-strictly-avoid-using-enums-on-android
大体的意思是,在如下情况下可以考虑用枚举

  • 需要类型检查的时候,我们只能接受某些非连续的值的情况,使用枚举变量可以有效避免传入非安全的值。
  • 更多的数据,你的数据包含不止一个信息并且这些信息不能放在一个变量中的时候。
  • 复杂的数据,你的数据需要一些操作方法作用在它上面的时候。

除了这些情况大家尽量不使用枚举,简单来说就是能不用enume 的尽量不用,那么我们是否能够克服使用静态常量的问题呢?肯定是有办法的,下面就介绍下Android中的替代方案:

Android中的替代方案

public class Sexs {
@IntDef({MALE, FEMALE})
@Retention(RetentionPolicy.SOURCE)
public @interface PersonSex{}

public static final int MALE = 0;
public static final int FEMALE = 1;
}
public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
Person person = new Person();
person.setSex(MALE);
((Button) findViewById(R.id.test)).setText(person.getSexDes());
}
class Person {

@Sexs.PersonSex
private int sex;

public void setSex(@Sexs.PersonSex int sex) {
this.sex = sex;
}

@Sexs.PersonSex
public int getSex() {
return sex;
}

public String getSexDes() {
if (sex == MALE) {
return "男";
} else {
return "女";
}
}
}
}

@SEX注解可以放到属性定义,参数,返回值等地方对数据类型进行限制。

为什么Enume 不能继承其他类,也不能被其他类继承

为了说明这个问题我们先编写一个枚举类如下所示:

public enum EnumeTest {
LIGHT_STATUS_ON,LIGHT_STATUS_OFF
}

然后紧接着将该类编译成class文件,然后使用javap来反编译查看到底长啥样,下面就是反编译后的样子:

 public final class EnumeTest extends java.lang.Enum<EnumeTest> {
public static final EnumeTest LIGHT_STATUS_ON;
public static final EnumeTest LIGHT_STATUS_OFF;
public static EnumeTest[] values();
public static EnumeTest valueOf(java.lang.String);
static {};
}

大家应该知道为什么enume后面不能使用extends来继承其他类了吧,因为已经继承了Enum,Java中不支持多继承,那为啥不能被其他继承?因为是final 类。

枚举与混淆

混淆大家估计都懂吧,即使不懂也听说过这个概念,就是用于增强反编译的难度,基本上大多数的app在发布之前都会通过混淆处理,在默认的混淆配置文件中,已经加入了对枚举混淆的处理

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

使用proguard优化

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

-optimizations class/unboxing/enum

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

参考资料

http://www.ibm.com/developerworks/cn/java/j-lo-enum/
https://www.liaohuqiu.net/cn/posts/android-enum-memory-usage/
http://www.cnblogs.com/zgz345/p/5871351.html
http://stackoverflow.com/questions/29183904/should-i-strictly-avoid-using-enums-on-android
http://droidyue.com/blog/2016/11/29/dive-into-enum/index.html
http://www.jianshu.com/p/f8ac84a3e3c1

Contents
  1. 1. Enume 枚举类型的重要方法的使用
  2. 2. 自定义枚举变量并为自定义枚举类添加相关方法
  3. 3. 分类枚举的组织方式
  4. 4. EnumeSet 和 EnumeMap的使用
  5. 5. 如何避免错误使用 Enum
  6. 6. 在Android 开发中为什么需要节制使用枚举
  7. 7. Android中的替代方案
  8. 8. 为什么Enume 不能继承其他类,也不能被其他类继承
  9. 9. 枚举与混淆
  10. 10. 使用proguard优化
  11. 11. 参考资料