java 枚举深入理解

232 阅读25分钟

枚举在我们的开发中再常用不过了,但是如果浮于表面地使用那就太轻视它了,那么我们接下来就深入了解一下枚举。

一、枚举是什么

首先我们从生活中的枚举来入手,然后再类比Java中的概念,我们会发现“艺术来源于生活”。

对于生活中枚举的例子,比如说我们猜扑克牌点数,两次相加大于15 的获胜。我们如果要猜获胜的概率,那么在这个时候,就可以使用枚举的方法:即列出所有的可能,然后得出概率结果。

二、类比于Java 中的枚举

枚举是在java 5 中引入的。我们常用的枚举是限制变量的,同时它们只能是预先设定好的值。

使用枚举可以方便我们的开发,代码更直观,避免bug。

三、Java 中如何表示枚举

一个比较简单的枚举结构如下:

<public|private|protected> enum Name {
    item,
    item,
    item,
    ...
}

四、Java 中的使用规则

如果看到这篇文章,相信对于枚举也或多或少地有所了解。当然,枚举的使用也有其固定的规则,Java 的规则大致如下:

  • 类中的item 是确定的有限个数。
  • 开发的时候如果需要一组常量时,则应该直接使用枚举。
  • 当枚举类里面只有一个item,那么可以作为单例模式的实现方法。
  • 枚举类是不可以被继承的。
  • 枚举类不可以直接使用new 来创建对象。
  • 枚举类中的枚举成员是用,隔开的。
  • 如果枚举类中的只有枚举成员,没有别的东西,那么我们在用,隔开的同时,最后是可以省略;的。

五、Java 枚举的应用场景

在了解了Java 中使用枚举的使用规则之后,我们就可以在工作中进行实际应用了。一般来说,有以下几种常用的场景适合使用枚举类。

星期(日期):

  • Sunday(星期日)、Monday(星期一)、Tuesday(星期二)、Wednesday(星期三)、Thursday(星期四)、Firday(星期五)、Saturday(星期六)

性别:

  • Man(男)、Woman(女)

季节:

  • Spring(春天)、Summer(夏天)、Autumn(秋天)、Winter(冬天)

支付种类相关:

  • Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡) 电商订单相关的状态:
  • Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、Delivered(已发货)、Return(退货)、Checked(已确认)

等等......

六、枚举基本的使用步骤

在之的文章和上述内容中,我们已经差不多了解了枚举的作用。在Java 中,枚举的使用也一致,它也是用来列举出我们所需要的各个概念,然后方便我们取出来其中的一个或者多个元素进行使用。

那么问题来了,在Java 中,枚举是如何具体地使用的呢,它的语法是什么呢,有什么注意事项吗?

这里我们假设罗永浩的那个买咖啡的场景,他在买咖啡的时候,有三种杯型:中杯、大杯、超大杯。在这之外,是没有其他类型的。店里也不允许点其他杯型的饮品的。

在Java 开发中,我们可以使用代码来实现一下这个场景。

我们来创建枚举类,用来表示咖啡的各个杯型:

public enum CoffeeCup {
    MEDIUM, LARGE, SUPERLARGE
}

这样,就是一个简单且完整的可以表示咖啡杯型的枚举类

在有了枚举类之后,我们就应该想,如何将这个枚举应用到开发中,于是我们自然就会想到,去创建咖啡类,再新增方法,用来判断枚举中的中、大、超大杯。给出这些定义之后,再新增一个方法,给出最终罗永浩选择的杯型。

image.png

咖啡类代码如下:

public class CoffeeTest {
    
    public static void chooseCoffeeCup(CoffeeCup cupSize) {
        if (cupSize == CoffeeCup.SUPERLARGE) {
            System.out.println("选择了超大杯");
        } else if (cupSize == CoffeeCup.MIDDLE) {
            System.out.println("选择了大杯");
        } else {
            System.out.println("选择了小杯");
        }
    }
    
    public static void main(String[] args) {
        CoffeeCup middleSize = CoffeeCup.MIDDLE;
        CoffeeTest.chooseCoffeeCup(middleSize);
    }
}

运行一下,输出结果为:

选择了中杯

这个其实是枚举的最基本的使用,但是我们在语法中还介绍了一种在类中定义的枚举。

另外,还有一种可以定义在类中的枚举,还是相同的场景,可以定义如下:

public class CoffeeTest {
    enum CoffeeCup {
        MIDDLE,
        LARGE,
        SUPERLARGE
    }
}

这样就可以在类中去创建枚举了。

但是我们仔细推敲一下就会发现,这样其实并不合理,为什么这么说呢?这是因为我们写代码的时候,要遵循单一职责原则。

这个代码是在咖啡类中列举的中杯、大杯、超大杯这三种杯型的枚举成员的。

所以说,根据规范来看,在某种咖啡类中是不建议有这个枚举的,因为在生活中的咖啡店里喝的所有很多类型的咖啡都有这三种杯型,然后我们还要在所有咖啡种类中都写一个枚举类,这是很不合理的。当然了,如果有子类继承这个咖啡类进行新的开发,也是可以的。

然后我们继续思考,如何让它变的比较合理,我们可以创建咖啡枚举类来细分咖啡的种类;创建咖啡杯型枚举类来细分咖啡杯型。

七、自定义枚举类

关于之前文章中介绍的枚举的基本使用,对初学的读者来说可能会感到有一些不知所措,不知道为什么要这样去创建枚举对象。那么接下来,我们来试试使用常量自定义枚举。

在之前的文章我们列举了很多枚举的应用场景,那么现在我们从中挑选一个来进行自定义枚举。

这里我们选择季节的场景。

首先我们来创建一个四季类,然后提供其所有的属性、私有构造方法、春、夏、秋、冬四个常量、还有Getter方法以及toString 方法。代码如下:

public class FourSeason {
    private final String name;
    private FourSeason(String name) {
        this.name = name;
    }

    public static final FourSeason SPRING = new FourSeason("春");
    public static final FourSeason SUMMER = new FourSeason("夏");
    public static final FourSeason AUTUMN = new FourSeason("秋");
    public static final FourSeason WINTER = new FourSeason("冬");
    
    public String getName() {
        return name;
    }
    
    @Override
    public String toString() {
        return "this season is: " + name;
    }
}

在给出这个四季类之后,我们写一个用来测试的类,看看这个自定义的枚举是否好用。

public class FourSeasonTest {
    public static void main(String[] args) {
        FourSeason winter = FourSeason.WINTER;
        System.out.println(winter);
    }
}

因为我们复写了toString 方法,所以在打印WINTER 的时候,就默认调用了它的toString 方法,打印的结果为:

this season is: 冬

八、带有参数的枚举(使用enum 关键字)

如果你在之前介绍Java 枚举类的基本使用的时候不是太明白,那么我想如果在看完自定义枚举类之后,也差不多了解一些了。

但是这个时候就有个问题了,不知道你有没有注意到,我们的自定义枚举类在定义的时候,是使用的有参数的对象。

于是我们就可以顺着这个思路想一想,我们怎样使用真正的枚举(enum 关键字)来实现有参数的枚举的定义呢?我们来看一下。

这里我们先讲明,我们还是使用上一篇文章的场景,现在把自定义枚举类改造一下,将其改成使用enum 关键字的枚举,然后使用有参对象,代码如下:

public enum FourSeason {
    SPRING("春"),
    SUMMER("夏"),
    AUTUMN("秋"),
    WINTER("冬");
    
    private final String name;
    
    FourSeason(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}

细心的读者可能发现了,对比上一篇文章的自定义枚举,除了关键字的变化,我们还有以下几处变化:

  • 第一个就是也就是最明显的就是,我们使用了enum 关键字代替了class
  • 然后就是我们创建的常量item 部分,不是每个都是分号,而是使用逗号隔开的,在最后一个item 才使用 的分号
  • 这个枚举中没有重写toString 方法

关于不重写toString 方法具体原因解释,我们后续介绍。


在上面最后我们留了一个问题,即为什么不重写toString 方法。

我们首先写一个测试类,来验证一下带参数的枚举的效果,代码如下:

public class FourSeasonTest {
    public static void main() {
        FourSeasonTest summer = FOURSEASON.SUMMER;
        System.out.println(summer.toString());
        System.out.println(summer.getName());
        System.out.println(summer);
    }
}

最后输出的结果为:

SUMMER
夏
SUMMER

然后,我们在运行完测试类之后,发现在没有重写toString 方法的情况下,打印出来的竟然是SUMMER,读者可能就疑惑了,这到底是为什么呢?

这个时候我们就该考虑到代码中的继承关系了,我们可以从源码中的继承关系进行分析。

如果继承的是基类Object 的话,那么在使用者没有重写toString 方法的情况下,系统会直接打印出来对象的地址。

所以说,我们就可以以此断定,enum 枚举的父类肯定不是Object 类,它肯定有一个父类继承了Object 类,并重写了toString 方法。

那么enum 的父类到底是哪个类呢?我们现在就来分析一下,我们可以借助它的对象来获取它的父类,操作如下:

// 在上面的测试类中可以加上这段代码
System.out.println(FourSeason.class.getSuperclass());

结果为:

class java.lang.Enum

执行完成之后,我们发现打印出的它默认继承的是Enum 类。那么我们在稍后就去看看这个类中到底写了些什么方法,这个类到底是实现了什么样的功能。

七、Enum 类中常用方法

首先我们来看看Enum 中的所有方法都包含了什么,代码如下:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

    public final String name() {
        return name;
    }

    private final int ordinal;

    public final int ordinal() {
        return ordinal;
    }

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

    public String toString() {
        return name;
    }

    public final boolean equals(Object other) {
        return this==other;
    }

    public final int hashCode() {
        return super.hashCode();
    }

    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

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

    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }

    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

    protected final void finalize() { }

    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }
}

我们现在挑其中比较重点的方法分别介绍一些他们的功能: name: 这个方法的作用就是获得枚举成员的名称。

valueof: 它用来获取指定枚举成员名称和类型的枚举成员。

values: 使用这个方法可以获取枚举成员的所有值。

compareTo: 它用作比较此枚举与指定对象的顺序。

hashCode: 使用这个方法得到枚举成员的哈希值。

ordinal: 调用这个方法能得到枚举成员的序数(第一个序数为0)。

toString: 用来返回枚举成员名称。

getDeclaringClass: 返回枚举成员的类对象。

(1) toString 方法和name 方法

对于name 方法和toString 这两个方法,它们的作用其实非常简单。

name 方法其实就是根据我们定义的枚举成员,来获取该枚举成员的字符串名称。

对于toString 方法,功能也差不多一样,也是用来获取枚举成员的字符串名称。

虽然这两个方法的作用是相同的,但是name 方法是用final 修饰的,它是不能被重写的;但是toString 方法没有final 的修饰,它是可以被重写的。

在这里为了让读者更直观地感受一下,我们还是使用前面文章的季节的场景来演示一下效果,我们来打印相应的结果,代码如下:

System.out.println(Season.SUMMER.name());
System.out.println(Season.SUMMER.toString());

输出结果为:

SUMMER
SUMMER

(2) valueOf

这个方法的作用根据名字也可以直观地看出来,它的作用是根据传入的一个字符串,将它转换成定义的对应的枚举成员。

使用这个方法传入的字符串必须与定义的枚举成员的名称一致,同时它严格区分大小写。

在传入的字符串并没有找到对应的枚举成员对象的时候,就会抛出异常。

上面的概念说起来可能有些拗口,我们继续使用实例来做展示,代码如下:

System.out.println(Season.valueOf("秋"));
System.out.println(Season.valueOf("abc"));

输出结果为:

AUTUMN
// 第二个抛异常
java.lang.IllegalArgumentException

(3) values

values 方法的名字中带有一个字母s,读者应该不陌生这个字母的意思,在英文中是“复数”。

同时我们看它的方法签名,它返回的值是一个字符串数组。于是我们就可以猜到,它的作用是获取枚举成员的所有的值,最终以数组的形式存储并返回。

按照惯例,我们依旧给出代码来做形象地说明,还是四季的场景,代码如下:

FourSeason[] seasons = FourSeason.values();
seasons.forEach(System.out::println);

输出结果为:

SPRING
SUMMER
AUTUMN
WINTER

这样依赖,这个方法的作用就很明确了,读者可以自行试着实践一下。

(4) ordinal

这个该方法的作用是获取枚举成员的序数。

对于第一个枚举成员的位置是0。当然,有一些比较有经验的或者比较聪明的读者可能就会联想到,这就是元素的序数。所以说,为了好理解,我们可以直接把它看作数组中的索引。

数组中的第一个元素位置同样也是从0 开始,ordinal 方法返回的也是一样。

我们继续用代码实际打印一下,来看看结果是什么,代码如下:

System.out.println(FourSeason.AUTUMN.oridinal());
System.out.println(FourSeason.SPRING.oridinal());
System.out.println(FourSeason.WINTER.oridinal());

FourSeason[] seasons = FourSeason.values();
for (FourSeason season: seasons) {
    System.out.println(season.oridinal());
}

输出结果为:

2
0
3
0
1
2
3

这样一来,这个方法的作用也很直观了。

对于oridinal 方法,它的源码其实就是返回了一个从0 开始int 类型的值,有兴趣的读者可以去看一下。

(5) compareTo

在日常开发中,compareTo 方法对我们来说,相信是一个很常用的方法,同时我们一定也是很熟悉了。

这个方法的作用很直白,它就是用来比较的。如果是数字,这个很容易理解,但是如果放到枚举中,读者可能就会表示疑惑了,在枚举中它比较的是什么呢,在枚举中有什么可比较的呢?

我们可以回顾一下,之前介绍的那些方法中,有哪些是可以用来做比较的呢?

细心的读者可能已经发现了,只有在介绍oridinal() 方法的时候涉及到“序数”的概念,实际上,在枚举中,compareTo 方法比较的就是表示两个枚举成员的次序的数字,但是有一点是需要注意到,compareTo 方法的返回值是次序相减后的结果,而不仅仅是0 / 1 / -1 这三个数字。

我们依旧是使用四季的场景来介绍。

首先,我们在前面介绍的文章中知道了,枚举的叙述是从0 开始的,所以说SPRING 的次序数为0,最后一个枚举元素WINTER 的次序数是3。

当使用前者比较后者的时候,打印出来的结果就是前者与后者的序数相减后的差值,即“0 - 3 = -3”。

口说无凭,我们继续用代码来实践一下:

System.out.println(FourSeason.SPRING.compareTo(FourSeason.WINTER));

输出结果为:

-3

八、Java 枚举中的高级属性

(1)常量的表示

在我们日常开发中,我们知道,在表示常量的时候,是用public static final来修饰的。

在Java5 之后有了枚举,我们就可以把相关的一些常量放在同一个枚举容器中,这样就比较方便了。另外,使用枚举的好处还在于枚举可以给我们提供很多便捷的的方法,对我们的开发更友好。(即之前几篇文章中介绍的那些方法)。

继续使用四季的场景来做说明,常量可以这么表示:

public enum FourSeason {
    SPRING, SUMMER, AUTUMN, WINTER
}

(2) 枚举结合switch 语句使用

首先我们思考一下,你印象中的switch 语句是什么样的结构呢?switch 语句都支持哪些类型呢?

在这里就直接说一下switch 语句支持的几种类型:

  • 基本数据类型:byte、short、char、int
  • 包装数据类型:Byte、Short、Character、Integer
  • 枚举类型:enum
  • 字符串类型:String(在jdk7 之后才开始支持的)

由于我们文章的主题就是枚举类型,所以其他类型的switch 语句就不做介绍了。

那么具体来说,枚举类型与switch 语句是如何配合的呢?枚举结合switch 语句的使用可以带来什么方便之处呢?我们继续使用实例来介绍一下。

public class SwitchWithEnumTest {
    public static void main(String[] args) {
        FourSeason season = FourSeason.Summer;
        switch(season) {
            case SPRING:
                System.out.println("春");
                break;
            case SUMMER:
                System.out.println("夏");
                break;
            case AUTUMN:
                System.out.println("秋");
                break;
            case WINTER:
                System.out.println("冬");
                break;
            default:
                System.out.println("null");
        }
    }
}

运行一下代码,输出结果为:

(3) 在枚举中定义多个参数,定义多个方法

在之前我们演示过,在枚举中可以定义参数,想法多的读者可能就会想了,是否可以定义多个参数呢?答案是可以的。这篇文章我们就来看一下如何定义多个参数。同时,除了可以定义多个参数以外,还可以在枚举中定义多个方法。

这里我们就做个简单但演示,代码如下:

public enum FourSeason {
    SPRING("春"),
    SUMMER("夏"),
    AUTUMN("秋"),
    WINTER("冬");
    private final String name;
    public static String getName(int index) {
        for(FourSeason s : FourSeason.values()) {
            if (index == s.getIndex()) {
                return s.name();
            }
        }
        return null;
    }
    FourSeason(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

在这个枚举中,我们新增了通过序号来获取对应的枚举name 的方法。当然,读者可以根据自己的需求定义自己想要的方法,这都是可以的。

(4) 通过枚举实现接口

最开始的文章中我们就提到过,枚举是没有继承功能的,虽然没有继承功能,但是它是可以实现接口的。以下我们使用代码来做一个简单的介绍。

首先,我们随便创建一个接口(这里仅仅是为了演示它的实现接口的功能):

public interface TakeATest {
    void test();
}

然后我们继续使用四季枚举,实现这个接口,代码如下:

public enum FourSeason implements TakeATest {
    SPRING("春"),
    SUMMER("夏"),
    AUTUMN("秋"),
    WINTER("冬");
    private final String name;
    public static String getName(int index) {
        for(FourSeason s : FourSeason.values()) {
            if (index == s.getIndex()) {
                return s.name();
            }
        }
        return null;
    }
    FourSeason(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    @Override
    public void test() {
        System.out.println("take a test");
    }
}

然后我们是如何使用的呢,代码如下:

FourSeason.WINTER.test();

输出结果为:

take a test

但是这么写,我们可以发现使用枚举实现接口的那种做法有一些不好。因为我们不管在使用哪一个枚举成员的时候,调用show 方法之后,返回的都是同一个值。

所以,我们可以变通一下,我们在实现接口后,重写的规则可以根据枚举成员绑定,代码如下:

public enum FourSeason implements TakeATest {
    SPRING("春") {
        @Override
        public void test() {
            System.out.println("take a test for spring");
        }
    },
    SUMMER("夏") {
        @Override
        public void test() {
            System.out.println("take a test for summer");
        }
    },
    AUTUMN("秋") {
        @Override
        public void test() {
            System.out.println("take a test for autumn");
        }
    },
    WINTER("冬") {
        @Override
        public void test() {
            System.out.println("take a test for winter");
        }
    };
    private final String name;
    public static String getName(int index) {
        for(FourSeason s : FourSeason.values()) {
            if (index == s.getIndex()) {
                return s.name();
            }
        }
        return null;
    }
    FourSeason(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

这种方式我们在初看起来可能感觉有点奇怪,即我们在枚举成员的后面加了大括号{}

重写的方法可以写在各个枚举成员后面中,这样就不会再有了上面说的那个限制。

在这之后,我们再使用这个枚举的时候,通过不同的枚举成员对象调用的show 方法,得到的结果都是不同的。

(5) 通过接口对枚举进行分类

这个特性当然是一个承上启下的特性。通过之前的学习,我们知道了java 中的枚举是可以实现接口的。但是,如果我们想通过接口对枚举进行分类,实现的方法还是有一些需要学习的地方的。

首先我们需要创建一个接口容器,在这个容器里面存放着此接口容器的多个枚举类,然后让各个枚举类实现这个接口,以这样的方式就可以实现对枚举分类的功能。

看起来还是比较拗口,我们已经身体力行,使用代码展示一下对应的功能:

public interface Seasons {
    enum ColdSeason implement Seasons {
        SPRING, WINTER
    }
    enum WarmSeason implement Seasons {
        SUMMER, AUTUMN
    }
}

然后我们实现一个测试类,代码如下:

class SeasonTest {
    public static void main(String[] args) {
        System.out.println("比较冷的季节:");
        for (Seasons.ColdSeason coldSeason : Seasons.ColdSeason.values()) {
            System.out.println(coldSeason);
        }
        System.out.println("比较温暖的季节:");
        for (Seasons.WarmSeason warmSeason : Seasons.WarmSeason.values()) {
            System.out.println(warmSeason );
        }
    }
}

执行一下代码,输出的结果为:

比较寒冷的季节:
SPRING
WINTER
比较温暖的季节:
SUMMER
AUTUMN

九、枚举类集合

EnumSet

首先我们来介绍EnumSet。看到这个词我们下意识地就会得到一个信息,这个是和Set 集合有关的。的确,关于Set集合,我们知道其集合中的元素是不重复的。其中的方法有以下几种:

(我们一边介绍这些方法,一边介绍方法的功能)

allOf 方法

它的返回值类型是EnumSet,作用是创建一个包含了指定元素类型的所有元素的枚举的set。

clone 方法

它的返回值也是EnumSet,用它来返回一个set 集合。

complementOf 方法

它的参数是EnumSet,返回值类型是EnumSet,它用来创建一个元素类型与指定枚举set 相同的set 集合。在这个方法中,新集合中的内容是原集合所不包含的枚举成员。

copyOf 方法

它的参数是EnumSet,返回值类型是EnumSet,调用它可以完成创建一个其元素类型与指定枚举set 中类型相同的枚举set 的集合。这个方法中的新集合中包含与原集合相同的枚举成员。

另外一个copyOf 方法,它的参数是Collection,返回值类型是EnumSet,它的作用是创建一个从指定 collection 初始化的枚举set。

noneOf 方法

它的参数为Class,返回值类型是EnumSet,可以用它来创建一个具有指定元素类型的空枚举 set。

range 方法

它有两个参数,都是泛型,分别为from & to,它的作用是创建一个包含两个指定端点所定义范围内所有元素的枚举set。

of 方法

它的参数是n 个泛型参数,返回值类型为EnumSet,它用来创建一个包含指定元素的枚举set。这里需要注意,它的参数可以有多个。

allOf 方法

allOf方法需要我们传入一个枚举对象,它会根据传入的枚举对象来生成一个具有该类中的所有枚举成员的Set集合。

这里我们继续使用代码来展示它的功能,创建一个包含FourSeason 中所有枚举元素的Set 集合,代码如下:

EnumSet<FourSeason> set = EnumSet.allOf(FourSeason.class);
System.out.println(set);
for (FourSeason season: set) {
    System.out.println(season);
}

输出结果如下:

[SPRING, SUMMER, AUTUMN, WINTER]
SPRING
SUMMER
AUTUMN
WINTER

通过结果可以分析出来,这个方法其实也是十分易懂的。

clone 方法

clone 方法得到的结果其实和直接打印枚举的Set 集合的结果一致,代码如下:

EnumSet<FourSeason> set = EnumSet.allOf(FourSeason.class);
System.out.println(set.clone());

执行代码之后,结果为:

[SPRING, SUMMER, AUTUMN, WINTER]

range 方法

之前的文章有介绍过,枚举其实是有序数的。

对于枚举中的枚举成员的顺序是从左向右的。根据此性质,我们就可以做很多事情。关于range 方法的作用就是可以使用它来创建指定枚举成员中端点的Set集合。它的两个参数是from 和to,也就是说我们需要传入枚举成员的起始端点与结束端点,根据这两个端点中的元素内容去创建一个拥有该范围内的枚举成员的Set 集合。代码如下:

EnumSet<FourSeason> set = EnumSet.allOf(FourSeason.class);
System.out.println(EnumSet.range(set.SUMMER, set.AUTUMN));

执行结果为:

[SUMMER, AUTUMN]

可能光看解释,不是很清晰,但是看完代码输出的结果之后,方法的含义就十分明显了。这个方法对我们来说也十分直观。

complementOf 方法

这个方法相对于之前介绍的那些方法来说,有点特殊,它会根据EnumSet 来创建一个新Set集合。对于新的Set 集合中的枚举成员其实是旧Set 集合中枚举成员的取反。(complement 的意思就是“补充,补足”的意思)

这里我们继续使用季节的场景来模拟一下,当前FourSeason 枚举中有春夏秋冬四个季节。首先我们直接使用allOf 方法来得到一个EnumSet 集合,然后针对这个集合使用complementOf 方法查看得到的新的EnumSet 的结果,发现输出为一个空集合:[];然后我们使用range 方法创建一个从夏到秋的Set 集合(set1),然后我们立即使用complementOf 方法根据set1 生成新的集合(set2),最后打印s2,查看集合中的元素,只有春、冬。

另外,需要说明的是,如果我们的旧Set 集合占据了枚举中的全部枚举成员,然后我们使用complementOf 方法生成的新集合,打印新集合中的元素,发现结果为空:[]。

我们来看一下代码:

EnumSet<FourSeason> seasons = EnumSet.allOf(FourSeason.class);
EnumSet<FourSeason> complementSeasonSet = EnumSet.complementOf(seasons); System.out.println(complementSeasonSet);
EnumSet<Week> range = EnumSet.range(FourSeason.SUMMER, FourSeason.AUTUMN); EnumSet<Week> complementSeasonSet2 = EnumSet.complementOf(range); System.out.println(complementSeasonSet2);

执行一下代码,输出结果为:

[]
[SPRING, WINTER]

对于complementOf 方法,通过上面的介绍也就比较直观了。我们可以看得出来,这个“取反”的操作就相当于集合概念中的“补集”。对于这种方法,可能我们在实际工作开发中使用不到,但是这并不是我们不去了解的原因。

copyOf 方法

对于copyOf 方法和上一篇的complementOf 对比一下,发现它们的功能正好相反。copyOf 的方法是创建一个新Set 集合。而对于这个新Set集合中的枚举成员与旧Set 集合中的枚举成员是相同的,这个功能和方法的名字一样,相当于就是Copy(复制)。

另外我们需要注意,copyOf 方法还有一个重载方法,可以复制Collection 集合来创建Set集合,其Collection 集合中必须存储的是枚举成员。

如果根据上一篇文章的介绍,你理解了complementOf 方法,这个方法也是很好理解的。这里我们直接上代码,可以更直观一些:

EnumSet<FourSeason> seasons = EnumSet.allOf(FourSeason.class); 
EnumSet<FourSeason> copiedSeasons = EnumSet.copyOf(seasons); 
System.out.println(copiedSeasons);

Set set = new HashSet();
set.add(FourSeason.SUMMER);
set.add(FourSeason.WINTER);
EnumSet copiedSet2 = EnumSet.copyOf(set);
System.out.println(copiedSet2);

执行代码,输出结果为:

[SPRING, SUMMER, AUTUMN, WINTTER]
[SUMMER, WINTER]

of 方法

对于of 方法的介绍其实很简单,它就为我们提供了非常便利的选择性,我们可以挑选枚举中的任意枚举成员作为Set集合的元素。

代码如下:

EnumSet set = EnumSet.of(FourSeason.SUMMER, FourSeason.WINTER);
System.out.println(set);

输出结果为:

[SUMMER, WINTER]

noneOf 方法

这个方是根据传入的一个枚举的类对象去创建一个空Set集合,代码如下:

EnumSet<FourSeason> noneSet = EnumSet.noneOf(FourSeason.class);
System.out.println(noneSet)

输出结果为:

[]

EnumMap 集合


首先我们来介绍一下EnumMap 集合的各个方法。看到这个名字我们知道,它是和Map 有关的一个集合结构。我们都知道,关于Map集合,它是由键和值组成。对于EnumMap 集合与HashMap 集合的效率比较来看,EnumMap的效率会高一些。具体原因有兴趣的读者可以去查阅一下。

EnumMap 的各个方法

其实关于EnumMap 集合的使用和HashMap 是差不多的,并没有什么特殊的地方。但是本着细致的原则,这里还是介绍一下:

clear 方法,没有参数,没有返回,它的作用是移除所有映射关系。

clone 方法,没有参数,返回一个EnumMap,它的作用是返回EnumMap 集合。

containsKey 方法,它的参数是一个Object,返回值是boolean,它用来判断是否包含此键,如果包含则返回true;反之返回false。

containsValue 方法,参数是一个Object,返回值是boolean,对比与containsKey,它用来判断集合是否包含一个或多个键映射到的该指定值,是则返回true;反之返回false。

entrySet 方法,无参数,返回Set,它的作用是返回映射键值关系的Set集合。

equals 方法,参数是Object,返回值为boolean,它用来比较对象与映射的相等关系。

get 方法,参数是Object,传入一个key,返回一个V,它用来获取指定键映射的值,如果没有这个key,那么返回null。

keySet 方法,没有参数,返回一个Set,这个集合包含了所有的键。

put(K key, V value) 方法,用来将指定键值存储在EnumMap 集合中去,返回value。

putAll,参数是一个Map,无返回值,它的作用是将所有键值对存储在集合中。