enum枚举

枚举是JDK5之后加入Java中的,泛型也是1.5之后加入的,但是与Java泛型的伪泛型不好的口碑相比,enum枚举的加入似乎是要好的多。

一.enum为什么会出现

在JDK5之前,如果想要表示一组特定的离散值,往往使用一些常量。比如:

package com.wangcc.JDK8.learnenum;

public class Entity {

    public static final int VIDEO = 1;// 视频
    public static final int AUDIO = 2;// 音频
    public static final int TEXT = 3;// 文字
    public static final int IMAGE = 4;// 图片

    private int id;
    private int type;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

}

这样做都有很多的坏处。这些常量通常都是连续、有无穷多个值的量,而类似这种表示类别的量则是离散的,并且通常情况下只有有限个值。用连续的量去表示离散量,会产生很多问题。例如,针对上述的Entity类,如果要对Entity对象的type属性进行赋值,一般会采用如下方法:

Entity e = new Entity();  
e.setId(10);  
e.setType(2);  

这样做的缺点有:(1)代码可读性差、易用性低。由于setType()方法的参数是int型的,在阅读代码的时候往往会让读者感到一头雾水,根本不明白这个2到底是什么意思,代表的是什么类型。当然,要保证可读性,还有这样一个办法:

e.setType(Entity.AUDIO);  

而这样的话,问题又来了。这样做,客户端必须对这些常量去建立理解,才能了解如何去使用这个东西。说白了,在调用的时候,如果用户不到Entity类中去看看,还真不知道这个参数应该怎么传、怎么调。像是setType(2)这种用法也是在所难免,因为它完全合法,不是每个人都能够建立起用常量名代替数值,从而增加程序可读性、降低耦合性的意识。
(2)类型不安全。在用户去调用的时候,必须保证类型完全一致,同时取值范围也要正确。像是setType(-1)这样的调用是合法的,但它并不合理,今后会为程序带来种种问题。也许你会说,加一个有效性验证嘛,但是,这样做的话,又会引出下面的第(3)个问题。
(3)耦合性高,扩展性差。假如,因为某些原因,需要修改Entity类中常量的值,那么,所有用到这些常量的代码也就都需要修改——当然,要仔细地修改,万一漏了一个,那可不是开玩笑的。同时,这样做也不利于扩展。例如,假如针对类别做了一个有效性验证,如果类别增加了或者有所变动,则有效性验证也需要做对应的修改,不利于后期维护。

枚举就是为了这样的问题而诞生的。它们给出了将一个任意项同另一个项相比较的能力,并且可以在一个已定义项列表中进行迭代。枚举(在Jave中简称为enum)是一个特定类型的类。所有枚举都是Java中的新类java.lang.Enum的隐式子类。此类不能手工进行子类定义。

二.enum的简单使用

我们来看一个最简单的枚举类。

package com.wangcc.JDK8.learnenum;

public enum TypeEnum {
    VIDEO, AUDIO, TEXT, IMAGE
}

在Java中要使用枚举,必须要使用enum关键字代替class关键字。
每一个枚举类都隐式的实现了java.lang.Enum这个抽象类。所以枚举类型不可以继承其他类,可以实现接口。我们通过反编译javap可以看到enum类的实际实现。
这里写图片描述
通过使用枚举类之后,我们可以对之前的Entity类进行改进。

package com.wangcc.JDK8.learnenum;

public class Entity {

    private int id;
    private TypeEnum type;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public TypeEnum getType() {
        return type;
    }

    public void setType(TypeEnum type) {
        this.type = type;
    }
}

在为Entity对象赋值的时候,就可以这样

Entity e = new Entity();  
e.setId(10);  
e.setType(TypeEnum.AUDIO);  

在调用setType()时,可选值只有四个,否则会出现编译错误,因此可以看出,枚举是类型安全的,不会出现取值范围错误的问题。同时,客户端不需要建立对枚举中常量值的了解,使用起来很方便,并且可以容易地对枚举进行修改,而无需修改客户端。如果常量从枚举中被删除了,那么客户端将会失败并且将会收到一个错误消息。枚举中的常量名称可以被打印,因此除了仅仅得到列表中项的序号外还可以获取更多信息。这也意味着常量可用作集合的名称,例如HashMap。

三.没那么神秘,enum也是Java类。

虽然enum枚举,看起来与一般的Java类有较大差距,但是enum始终也还是一个隐式继承了java.lang.Enum抽象类的类,所以呢,它也是可以有属性方法,可以实现接口的(Enum抽象类已经实现了Serializable可训序列化接口)。那我们就来看看怎么在enum枚举中添加方法和属性。再反编译下看看。

package com.wangcc.JDK8.learnenum;

public enum TypeEnum {
    VIDEO(1, "视频"), AUDIO(2, "音频"), TEXT(3, "文本"), IMAGE(4, "图像");

    int value;
    String name;

    TypeEnum(int value, String name) {
        this.value = value;
        this.name = name;
    }

    public int getValue() {
        return value;
    }

    public String getName() {
        return name;
    }
}

javap 反编译后:
这里写图片描述
这段反编译的代码中其实有一部分代码没有反编译出来,下面补全一下:
(如果枚举量有伴随参数并且手动添加了构造器,那么将会解析成一个静态的代码块在类加载时对变量进行初始化。)

public static final com.wangcc.JDK8.learnenum.TypeEnum VIDEO;
static{
VIDEO=new TypeEnum(1,"视频");
}
private TypeEnum(int value, String name) {
        this.value = value;
        this.name = name;
    }

enum中的构造方法的访问权限类型只能是private,这一点让我们想到了什么,单例模式,对enum枚举就是天然的单例模式。这也是大师Josh Bloch在《Effective Java》推荐的单例模式的最佳实现,不仅实现了线程同步,还能防止反序列化重新创建新的对象。(这一点是怎么做到的呢,我们在单例模式的博客中会进行讲解)

四.java.lang.Enum留给enum的财富

之前,我们说到enum枚举类型都是继承了Enum这个抽象类。那我们看看这个抽象类中有哪些方法可以供我们的枚举类型使用吧。
这里写图片描述
我们先分析下name()和ordinal()方法,基于之前的TypeEnum写一个测试类

public class TypeEnumTest {
    @Test
    public void test() {
        TypeEnum type = TypeEnum.VIDEO;
        System.out.println(type.getName());
        System.out.println(type.getValue());
        System.out.println(type.ordinal());
        System.out.println(type.name());
    }
}

得到的结果为

视频
1
0
VIDEO

Enum的子类TypeEnum的实例调用父类的name()方法,是怎么返回值的呢?我们看代码,好像并没有给父类的name属性和ordinal属性赋值呀。
其实是隐性的赋值了的,我们知道,子类继承父类,必须要实现父类的构造方法。由于Enum类中只有

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

构造方法,也就意味着隐式的无参构造方法已经不存在了。所以,作为Enum的子类,TypeEnum必须显式的实现父类的构造方法,而且必须是在构造方法的第一句声明(这个属于很基础的Java知识了,先初始化父类再初始化子类)。
所以,之前,我们说反编译后漏掉的一些语句,其实那些还不完整,应该说是错的,因为之前确实也没考虑到这一点。应该为:

public static final com.wangcc.JDK8.learnenum.TypeEnum VIDEO;
static{
VIDEO=new TypeEnum(1,"视频");
}

private TypeEnum(int value, String name) {
super("VIDEO",0);
        this.value = value;
        this.name = name;
    }

由于Enum实现了Comparable接口,所以,枚举类型可以通过compareTo(E)方法来比较元素的顺序。

    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }

Class getDeclaringClass()
返回与此枚举常量的枚举类型相对应的 Class 对象。
还有readObject和readObjectNoData方法,我们会在单例模式的相关博客中具体分析。

五.enum相应集合的应用

· EnumMap
 EnumMap的key不允许为null,value可以为null,按照key在enum中的顺序进行保存,非线程安全。
 底层的数据结构比较简单:
 一个对象数组,数组的下标索引就是根据Map中的key直接获取,即枚举中的ordinal值;效率比HashMap高,可以直接获取数组下标索引并访问到元素(HashMap源码分析得重新写一份了,之前用的JDK7版本,现在JDK8版本,当大于8个元素的时候,就不再是数组加链表的底层数据结构组成了,就变成了数组加红黑树了)。
 1)简单实用:

package com.wangcc.JDK8.learnenum;

import java.util.EnumMap;
import java.util.Map;
import java.util.Set;

import org.junit.Test;

public class EnumMapTest {
    @Test
    public void test() {
        Map<TypeEnum, String> map = new EnumMap<TypeEnum, String>(TypeEnum.class);
        map.put(TypeEnum.AUDIO, "AUDIO KOBE");
        map.put(TypeEnum.IMAGE, "IMAGE KOBE");
        map.put(TypeEnum.TEXT, "TEXT KOBE");
        map.put(TypeEnum.VIDEO, "VIDEO KOBE");
        Set<Map.Entry<TypeEnum, String>> entrySet = map.entrySet();
        for (Map.Entry<TypeEnum, String> entry : entrySet) {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
        System.out.println(map);
    }
}

2)什么时候用EnumMap
这个的答案,大师有给我们解答,《Effective Java》中有这样说到:
建议用EnumMap代替叙述索引,最好不要用序数来索引数组,而要使用EnumMap。
我们看书中的例子:

package com.wangcc.JDK8.learnenum;

public class Herb {
    public enum Type {
        ANNUAL, PERENNIAL, BIENNTAL
    }

    private final String name;
    final Type type;

    public Herb(String name, Type type) {
        this.name = name;
        this.type = type;
    }

    public Type getType() {
        return type;
    }

    @Override
    public String toString() {
        return name;
    }
}

 现在用一座种满香草的花园,想要按照类型(一年生、多年生、两年生,即上面Type的类型)进行组织之后将这些植物列出来。如果使用数组实现的话,需要构建三个集合,每种类型一个,并且遍历整座花园,将每种香草放到相应的集合中。

package com.wangcc.JDK8.learnenum;

import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.junit.Before;
import org.junit.Test;

public class HerbTest {
    Herb[] garden = null;

    @Before
    public void setup() {
        garden = new Herb[] { new Herb("f1", Herb.Type.ANNUAL), new Herb("f2", Herb.Type.PERENNIAL),
                new Herb("f3", Herb.Type.BIENNTAL), new Herb("f4", Herb.Type.PERENNIAL),
                new Herb("f5", Herb.Type.ANNUAL), new Herb("f6", Herb.Type.BIENNTAL), new Herb("f7", Herb.Type.ANNUAL),
                new Herb("f8", Herb.Type.BIENNTAL), new Herb("f9", Herb.Type.PERENNIAL) };
    }



    @Test
    public void testArray() {

        Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length];
        for (int i = 0; i < herbsByType.length; i++) {
            herbsByType[i] = new HashSet<Herb>();
        }
        for (Herb h : garden) {
            herbsByType[h.type.ordinal()].add(h);
        }
        for (int i = 0; i < herbsByType.length; i++) {
            System.out.printf("%s:%s%n", Herb.Type.values()[i], herbsByType[i]);
        }
    }
}

这个方法当然可以实现功能,但是存在很多问题,因为数组不能和泛型兼容,程序需要进行未受检的转换,并且不能正确无误地进行编译。因为数组不知道它的索引代表着什么,你必须手工标注这些索引的输出。但是这种方法最严重的问题在于,当你访问一个按照枚举的叙述进行索引的数组时,使用正确的int值就是你的职责了,int不能提供枚举的类型安全。 (这就是之前咱们讲的类型安全问题)。
我们可以通过EnumMap来改善这个程序。

@Test
    public void testEnumMap() {

        Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<>(Herb.Type.class);
        for (Herb.Type t : Herb.Type.values()) {
            herbsByType.put(t, new HashSet<Herb>());
        }
        for (Herb h : garden) {
            herbsByType.get(h.type).add(h);
        }
        System.out.println(herbsByType);
    }

代码更加简短,实现更加优美,也更安全,速度也快,因为底层是数组。
3)EnumMap的总结
EnumMap是专门为枚举类型量身定做的Map实现。虽然使用其它的Map实现(如HashMap)也能完成枚举类型实例到值得映射,但是使用EnumMap会更加高效:它只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值。这使得EnumMap的效率非常高。EnumMap在内部使用枚举类型的ordinal()得到当前实例的声明次序,并使用这个次序维护枚举类型实例对应值在数组的位置。

参考资料:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值