Java 集合补充

Posted 谁将新樽辞旧月,今月曾经照古人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合补充相关的知识,希望对你有一定的参考价值。

集合大致可以分List,Set,Queue,Map四种体系。

集合和数组不一样,数组元素可以是基本类型的值,也可以是对象(的引用变量),集合里只能保存对象(的引用变量)。

访问:如果访问List集合中的元素可以根据元素的索引,访问Map集合中的元素可以根据元素的key,访问Set集合中的元素只能根据元素本身来访问。

Collection操作:

public class CollectionTest
{
    public static void main(String[] args)
    {
        Collection c = new ArrayList();
        // 添加元素
        c.add("孙悟空");
        // 虽然集合里不能放基本类型的值,但Java支持自动装箱
        c.add(6);
        System.out.println("c集合的元素个数为:" + c.size()); // 输出2
        // 删除指定元素
        c.remove(6);
        System.out.println("c集合的元素个数为:" + c.size()); // 输出1
        // 判断是否包含指定字符串
        System.out.println("c集合的是否包含\\"孙悟空\\"字符串:"
            + c.contains("孙悟空")); // 输出true
        c.add("轻量级Java EE企业应用实战");
        System.out.println("c集合的元素:" + c);
        Collection books = new HashSet();
        books.add("轻量级Java EE企业应用实战");
        books.add("疯狂Java讲义");
        System.out.println("c集合是否完全包含books集合?"
            + c.containsAll(books)); // 输出false
        // 用c集合减去books集合里的元素
        c.removeAll(books);
        System.out.println("c集合的元素:" + c);
        // 删除c集合里所有元素
        c.clear();
        System.out.println("c集合的元素:" + c);
        // 控制books集合里只剩下c集合里也包含的元素
        books.retainAll(c);
        System.out.println("books集合的元素:" + books);
    }
}

 

 

注意:

JDK1.5以前系统吧集合所有的元素都当成Object类型,从1.5以后,可以使用泛型来限制集合元素的类型,并让集合记住所有集合元素的类型。

使用Lambda表达式遍历集合:

 Java8为Iterable接口新增了一个foreach(Consumer action)默认方法,该方法所需参数的类型是一个函数式接口,而Iterable接口是Collection接口的父接口,因此Collection集合也可以直接调用该方法。

当程序调用Iterable的foreach遍历集合元素,程序会依次将集合元素传给Consumer 的accep(T t)(该接口唯一的抽象方法)方法,因为Consumer是函数式接口,可以使用Lambda表达式来遍历集合元素:

public class CollectionEach
{
    public static void main(String[] args)
    {
        // 创建一个集合
        Collection books = new HashSet();
        books.add("轻量级Java EE企业应用实战");
        books.add("疯狂Java讲义");
        books.add("疯狂android讲义");
        // 调用forEach()方法遍历集合
        books.forEach(obj -> System.out.println("迭代集合元素:" + obj));
    }
}

 

最后一行调用了forEach()方法,传给该方法的参数是一个Lambda表达式,该Lambda表达式的目标类型是Comsumer。

使用Java8增强的Iterator遍历集合元素:

Iterator接口主要用于遍历Collection集合中的元素,Iterator也被称为迭代器。

next()方法返回迭代器刚刚经过的元素

hasNext()若返回True,则表明接下来还有元素,迭代器不在尾部。

remove()方法必须和next方法一起使用,功能是去除刚刚next方法返回的元素

forEachRemaining(Consumer action)是Java8新增的方法,可以使用Lambda表达式来遍历集合元素。

public class IteratorTest
{
    public static void main(String[] args)
    {
        // 创建集合、添加元素的代码与前一个程序相同
        Collection books = new HashSet();
        books.add("轻量级Java EE企业应用实战");
        books.add("疯狂Java讲义");
        books.add("疯狂Android讲义");
        // 获取books集合对应的迭代器
        Iterator it = books.iterator();
        while(it.hasNext())
        {
            // it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
            String book = (String)it.next();
            System.out.println(book);
            if (book.equals("疯狂Java讲义"))
            {
                // 从集合中删除上一次next方法返回的元素
                it.remove();
            }
            // 对book变量赋值,不会改变集合元素本身
            book = "测试字符串";   //
        }
        System.out.println(books);
    }
}

注意:

使用Iterator对集合元素进行迭代时,Iterator并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素本身没有影响。

 

当使用Iterator迭代访问Collection集合元素时,Iterator集合里的元素不能被改变,只有通过Iterator的remove方法删除上一次next方法返回的集合元素才可以,否则异常。

public class IteratorErrorTest
{
    public static void main(String[] args)
    {
        // 创建集合、添加元素的代码与前一个程序相同
        Collection books = new HashSet();
        books.add("轻量级Java EE企业应用实战");
        books.add("疯狂Java讲义");
        books.add("疯狂Android讲义");
        // 获取books集合对应的迭代器
        Iterator it = books.iterator();
        while(it.hasNext())
        {
            String book = (String)it.next();
            System.out.println(book);
            if (book.equals("疯狂Android讲义"))
            {
                // 使用Iterator迭代过程中,不可修改集合元素,下面代码引发异常
                books.remove(book);
            }
        }
    }
}

 

Iterator使用的时快速失败机制(fail-fast),一旦在迭代过程中检测到该集合已经被修改(通常是程序中的其他线程修改)程序立即引发异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题。

使用Lambda表达式遍历Iterator

Java8为Iterator新增的forEachRemaining(Consunmer action)方法,该方法所需的Consunmer 同样是函数式接口。

public class IteratorEach
{
    public static void main(String[] args)
    {
        // 创建集合、添加元素的代码与前一个程序相同
        Collection books = new HashSet();
        books.add("轻量级Java EE企业应用实战");
        books.add("疯狂Java讲义");
        books.add("疯狂Android讲义");
        // 获取books集合对应的迭代器
        Iterator it = books.iterator();
        // 使用Lambda表达式(目标类型是Comsumer)来遍历集合元素
        it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
    }
}

 

使用Java8新增的Predicate操作集合

Java8起Collection集合新增了一个removeIf(Predicate filter)方法,该方法将会批量删除符合filter条件的所有元素。该方法需要一个Predicate(谓词)对象作为参数,Predicate也是函数式接口,因此可以用Lambda表达式作为参数。

public class PredicateTest
{
    public static void main(String[] args)
    {
        // 创建一个集合
        Collection books = new HashSet();
        books.add(new String("轻量级Java EE企业应用实战"));
        books.add(new String("疯狂Java讲义"));
        books.add(new String("疯狂ios讲义"));
        books.add(new String("疯狂Ajax讲义"));
        books.add(new String("疯狂Android讲义"));
        // 使用Lambda表达式(目标类型是Predicate)过滤集合
        books.removeIf(ele -> ((String)ele).length() < 10);
        System.out.println(books);
    }
}

Predicate可以充分简化集合运算:

 

public class PredicateTest2
{
    public static void main(String[] args)
    {
        // 创建books集合、为books集合添加元素的代码与前一个程序相同。
        Collection books = new HashSet();
        books.add(new String("轻量级Java EE企业应用实战"));
        books.add(new String("疯狂Java讲义"));
        books.add(new String("疯狂iOS讲义"));
        books.add(new String("疯狂Ajax讲义"));
        books.add(new String("疯狂Android讲义"));
        // 统计书名包含“疯狂”子串的图书数量
        System.out.println(calAll(books , ele->((String)ele).contains("疯狂")));
        // 统计书名包含“Java”子串的图书数量
        System.out.println(calAll(books , ele->((String)ele).contains("Java")));
        // 统计书名字符串长度大于10的图书数量
        System.out.println(calAll(books , ele->((String)ele).length() > 10));
    }
    public static int calAll(Collection books , Predicate p)
    {
        int total = 0;
        for (Object obj : books)
        {
            // 使用Predicate的test()方法判断该对象是否满足Predicate指定的条件
            if (p.test(obj))
            {
                total ++;
            }
        }
        return total;
    }
}

使用Java8新增的Stream操作集合

Java8新增了Stream、IntStream、LongStream、DoubleStream等流式API。这些API代表多个支持串行和并行聚集操作的元素。其中Stream是一个通用的流接口,其他代表元素类型的流。

Java8还为每个流式API提供了对应的Builder。

public class IntStreamTest
{
    public static void main(String[] args)
    {
        IntStream is = IntStream.builder()
            .add(20)
            .add(13)
            .add(-2)
            .add(18)
            .build();
        // 下面调用聚集方法的代码每次只能执行一个
        System.out.println("is所有元素的最大值:" + is.max().getAsInt());
        System.out.println("is所有元素的最小值:" + is.min().getAsInt());
        System.out.println("is所有元素的总和:" + is.sum());
        System.out.println("is所有元素的总数:" + is.count());
        System.out.println("is所有元素的平均值:" + is.average());
        System.out.println("is所有元素的平方是否都大于20:"
            + is.allMatch(ele -> ele * ele > 20));
        System.out.println("is是否包含任何元素的平方大于20:"
            + is.anyMatch(ele -> ele * ele > 20));
        // 将is映射成一个新Stream,新Stream的每个元素是原Stream元素的2倍+1
        IntStream newIs = is.map(ele -> ele * 2 + 1);
        // 使用方法引用的方式来遍历集合元素
        newIs.forEach(System.out::println); // 输出41 27 -3 37
    }
}

 

public class CollectionStream
{
    public static void main(String[] args)
    {
        // 创建books集合、为books集合添加元素的代码与8.2.5小节的程序相同。
        Collection books = new HashSet();
        books.add(new String("轻量级Java EE企业应用实战"));
        books.add(new String("疯狂Java讲义"));
        books.add(new String("疯狂iOS讲义"));
        books.add(new String("疯狂Ajax讲义"));
        books.add(new String("疯狂Android讲义"));
        // 统计书名包含“疯狂”子串的图书数量
        System.out.println(books.stream()
            .filter(ele->((String)ele).contains("疯狂"))
            .count()); // 输出4
        // 统计书名包含“Java”子串的图书数量
        System.out.println(books.stream()
            .filter(ele->((String)ele).contains("Java") )
            .count()); // 输出2
        // 统计书名字符串长度大于10的图书数量
        System.out.println(books.stream()
            .filter(ele->((String)ele).length() > 10)
            .count()); // 输出2
        // 先调用Collection对象的stream()方法将集合转换为Stream,
        // 再调用Stream的mapToInt()方法获取原有的Stream对应的IntStream
        books.stream().mapToInt(ele -> ((String)ele).length())
            // 调用forEach()方法遍历IntStream中每个元素
            .forEach(System.out::println);// 输出8  11  16  7  8
    }
}

Set集合

set和Collection基本相同,实际上set就是Collection,只是行为略有不同(set不允许重复)。
如果把两个形同元素添加进set,会返回false,并元素不会被加入。

HashSet

HashSet是Set接口的典型实现,HasHSet按照Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。

特点:

无序。不同步,如果多个线程访问同一个HashSet,有两个或以上线程修改了时,必须通过代码保证其同步。集合元素可以null。

原理:

当HashSet集合存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后给根据hashCode值决定该对象的存储位置。判断两个元素相等的标准是2个对象的equals()方法比较相等,hashCode()方法返回值也相等。

// 类A的equals方法总是返回true,但没有重写其hashCode()方法
class A
{
    public boolean equals(Object obj)
    {
        return true;
    }
}
// 类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B
{
    public int hashCode()
    {
        return 1;
    }
}
// 类C的hashCode()方法总是返回2,且重写其equals()方法总是返回true
class C
{
    public int hashCode()
    {
        return 2;
    }
    public boolean equals(Object obj)
    {
        return true;
    }
}
public class HashSetTest
{
    public static void main(String[] args)
    {
        HashSet books = new HashSet();
        // 分别向books集合中添加两个A对象,两个B对象,两个C对象
        books.add(new A());
        books.add(new A());
        books.add(new B());
        books.add(new B());
        books.add(new C());
        books.add(new C());
        System.out.println(books);
    }
}

C类重写了equals()方法和hashCode()方法,返回相同的值,因此set集合中只有一个C类对象。

注意:

当一个对象放入Has和Set()集合时,如果需要重写该对象的equals()方法,则也应当重写该对象的hashCode()方法。规则是如果equals()返回true,则hashCode值也相等。

hash算法

散列算法(Hash Algorithm),又称哈希算法,杂凑算法,是一种从任意文件中创造小的数字「指纹」的方法。与指纹一样,散列算法就是一种以较短的信息来保证文件唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律。因此,当原有文件发生改变时,其标志值也会发生改变,从而告诉文件使用者当前的文件已经不是你所需求的文件。散列算法最重要的用途在于给证书、文档、密码等高安全系数的内容添加加密保护。这一方面的用途主要是得益于散列算法的不可逆性,这种不可逆性体现在,你不仅不可能根据一段通过散列算法得到的指纹来获得原有的文件,也不可能简单地创造一个文件并让它的指纹与一段目标指纹相一致。

一个优秀的 hash 算法,将能实现:

  • 正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。
  • 逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
  • 输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同。
  • 冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。

hash算法的功能是,保证快速查找被检索的对象,hash算法的价值在于速度,,当需要查询集合中的元素时,hash算法可以根据该元素的hashCode值计算出该元素的存储位置,从而快速定位该元素。

HashSet和数组:

数组有索引可以快速定位,HashSet集合里的元素没有索引,实际上当程序向HashSet集合中添加元素时,HashSet会根据该元素的hashCode值计算他的存储位置,这样也可以快速定位该元素。

优势:

数组元素的索引是连续的,数组长度是固定的,无法自由增加数组长度。HashSet因为使用hashCode值计算存储位置,从而可以自由增加HashSet长度,并根据hashCode值访问元素。因此,当从HashSet中访问元素时,HashSet先计算该元素的hashCode值,然后到hashCode值对应的位置取出该元素,所以速度快。

HashSet中每个能存储元素的槽位通常称为桶,如果有多个元素的hashCode值相同,但他们通过equals方法返回false,就会在同一个桶里放多个元素,导致性能下降。

如果像HashSet中添加一个可变对象后,后面程序修改了该可变对象的实例变量,可能导致它与集合中其他元素相同,这就可能导致HashSet中包含两个相同的对象。

class R
{
    int count;
    public R(int count)
    {
        this.count = count;
    }
    public String toString()
    {
        return "R[count:" + count + "]";
    }
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if (obj != null && obj.getClass() == R.class)
        {
            R r = (R)obj;
            return this.count == r.count;
        }
        return false;
    }
    public int hashCode()
    {
        return this.count;
    }
}
public class HashSetTest2
{
    public static void main(String[] args)
    {
        HashSet hs = new HashSet();
        hs.add(new R(5));
        hs.add(new R(-3));
        hs.add(new R(9));
        hs.add(new R(-2));
        // 打印HashSet集合,集合元素没有重复
        System.out.println(hs);
        // 取出第一个元素
        Iterator it = hs.iterator();
        R first = (R)it.next();
        // 为第一个元素的count实例变量赋值
        first.count = -3;     //// 再次输出HashSet集合,集合元素有重复元素
        System.out.println(hs);
        // 删除count为-3的R对象
        hs.remove(new R(-3));    //// 可以看到被删除了一个R元素
        System.out.println(hs);
        System.out.println("hs是否包含count为-3的R对象?"
            + hs.contains(new R(-3))); // 输出false
        System.out.println("hs是否包含count为-2的R对象?"
            + hs.contains(new R(-2))); // 输出false
    }
}

LinkedHashSet类

LinkedHashSet集合也是根据hashCode值来决定元素存储的位置,同时他使用链表维护元素的次序。性能低于HashSet,但是在迭代访问Set里的全部元素时将有很好的性能,因为他以链表来维护内部顺序。

public class LinkedHashSetTest {

    public static void main(String[] args)
    {
        LinkedHashSet books = new LinkedHashSet();
        books.add("疯狂Java讲义");
        books.add("轻量级Java EE企业应用实战");
        System.out.println(books);
        // 删除 疯狂Java讲义
        books.remove("疯狂Java讲义");
        // 重新添加 疯狂Java讲义
        books.add("疯狂Java讲义");
        System.out.println(books);
    }
}

TreESet类

TreESet时SortedSet接口的实现类,TreeSet可以保证集合元素处于排序状态。

public class TreeSetTest
{
    public static void main(String[] args)
    {
        TreeSet nums = new TreeSet();
        // 向TreeSet中添加四个Integer对象
        nums.add(5);
        nums.add(2);
        nums.add(10);
        nums.add(-9);
        // 输出集合元素,看到集合元素已经处于排序状态
        System.out.println(nums);
        // 输出集合里的第一个元素
        System.out.println(nums.first()); // 输出-9
        // 输出集合里的最后一个元素
        System.out.println(nums.last());  // 输出10
        // 返回小于4的子集,不包含4
        System.out.println(nums.headSet(4)); // 输出[-9, 2]
        // 返回大于5的子集,如果Set中包含5,子集中还包含5
        System.out.println(nums.tailSet(5)); // 输出 [5, 10]
        // 返回大于等于-3,小于4的子集。
        System.out.println(nums.subSet(-3 , 4)); // 输出[2]
    }
}

可以看出TreeSet是根据元素实际值大小排序的。

TreeSet采用红黑树的数据结构来存储集合元素。

自然排序:

TreeSet调用几何元素的compareTo(Object obj)方法比较元素的大小关系,然后升序排列。

Java提供了Comparable接口,里面提供了compareTo(Object obj)方法,该方法返回一个整数值,当返回0说明相等,正整数大于,负整数小于。

如果把一个对象添加到TreeSet时,该对象必须实现Comparable接口:

class Err{}
public class TreeSetErrorTest
{
    public static void main(String[] args)
    {
        TreeSet ts = new TreeSet();
        // 向TreeSet集合中添加Err对象
        // 自然排序时,Err没实现Comparable接口将会引发错误
        ts.add(new Err());
    }
}

想TreeSet添加的应该是同一个类的对象:

public class TreeSetErrorTest2
{
    public static void main(String[] args)
    {
        TreeSet ts = new TreeSet();
        // 向TreeSet集合中添加两个对象
        ts.add(new String("疯狂Java讲义"));
        ts.add(new Date());   //
    }
}

如果像TreeSet中添加的是自定义类,且自定义类实现了Comparable接口,且实现了compareTo(Object obj)方法没有进行强制转换,可以添加多种类型。

TreeSet判断相等的唯一标准是compareTo(Object obj)方法返回0。返回0则无法添加相同对象。

class Z implements Comparable
{
    int age;
    public Z(int age)
    {
        this.age = age;
    }
    // 重写equals()方法,总是返回true
    public boolean equals(Object obj)
    {
        return true;
    }
    // 重写了compareTo(Object obj)方法,总是返回1
    public int compareTo(Object obj)
    {
        return 1;
    }
}
public class TreeSetTest2
{
    public static void main(String[] args)
    {
        TreeSet set = new TreeSet();
        Z z1 = new Z(6);
        set.add(z1);
        // 第二次添加同一个对象,输出true,表明添加成功
        System.out.println(set.add(z1));    //// 下面输出set集合,将看到有两个元素
        System.out.println(set);
        // 修改set集合的第一个元素的age变量
        ((Z)(set.first())).age = 9;
        // 输出set集合的最后一个元素的age变量,将看到也变成了9
        System.out.println(((Z)(set.last())).age);
    }
}

注意:把一个对象放入TreeSet集合时,重写该对象的equals()方法时应保证该方法与compareTo()方法一致。

如果像TreeSet中添加一个可变对象并修改该对象的实例变量,导致它与其他对象的大小顺序发生了改变,但TreeSet不会再次调整他们的顺序,甚至可能他们的compareTo()结果返回0.

class R implements Comparable
{
    int count;
    public R(int count)
    {
        this.count = count;
    }
    public String toString()
    {
        return "R[count:" + count + "]";
    }
    // 重写equals方法,根据count来判断是否相等
    public boolean equals(Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        if(obj != null && obj.getClass() == R.class)
        {
            R r = (R)obj;
            return r.count == this.count;
        }
        return false;
    }
    // 重写compareTo方法,根据count来比较大小
    public int compareTo(Object obj)
    {
        R r = (R)obj;
        return count > r.count ? 1 :
                count < r.count ? -1 : 0;
    }
}
public class TreeSetTest3
{
    public static void main(String[] args)
    {
        TreeSet ts = new TreeSet();
        ts.add(new R(5));
        ts.add(new R(-3));
        ts.add(new R(9));
        ts.add(new R(-2));
        // 打印TreeSet集合,集合元素是有序排列的
        System.out.println(ts);    //// 取出第一个元素
        R first = (R)ts.first();
        // 对第一个元素的count赋值
        first.count = 20;
        // 取出最后一个元素
        R last = (R)ts.last();
        // 对最后一个元素的count赋值,与第二个元素的count相同
        last.count = -2;
        // 再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
        System.out.println(ts);   //// 删除实例变量被改变的元素,删除失败
        System.out.println(ts.remove(new R(-2)));   //
        System.out.println(ts);
        // 删除实例变量没有被改变的元素,删除成功
        System.out.println(ts.remove(new R(5)));    //
        System.out.println(ts);
    }
}

定制排序:

如果需要实现定制排序则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。Comparator是函数式接口,可以使用Lambda代替Comparator对象。

class M
{
    int age;
    public M(int age)
    {
        this.age = age;
    }
    public String toString()
    {
        return "M [age:" + age + "]";
    }
}
public class TreeSetTest4
{
    public static void main(String[] args)
    {
        // 此处Lambda表达式的目标类型是Comparator
        TreeSet ts = new TreeSet((o1 , o2) ->
        {
            M m1 = (M)o1;
            M m2 = (M)o2;
            // 根据M对象的age属性来决定大小,age越大,M对象反而越小
            return m1.age > m2.age ? -1
                : m1.age < m2.age ? 1 : 0;
        });
        ts.add(new M(5));
        ts.add(new M(-3));
        ts.add(new M(9));
        System.out.println(ts);
    }
}

EnumSet类

EnumSet是一个专为枚举设计的集合类。EnumSet也是有序的,EnumSet以枚举值在EnumSet类内的定义顺序来决定集合元素的顺序。

EnumSet在内部以位向量的形式存储,紧凑高效,占内存小,效率高。

enum Season
{
    SPRING,SUMMER,FALL,WINTER
}
public class EnumSetTest
{
    public static void main(String[] args)
    {
        // 创建一个EnumSet集合,集合元素就是Season枚举类的全部枚举值
        EnumSet es1 = EnumSet.allOf(Season.class);
        System.out.println(es1); // 输出[SPRING,SUMMER,FALL,WINTER]
        // 创建一个EnumSet空集合,指定其集合元素是Season类的枚举值。
        EnumSet es2 = EnumSet.noneOf(Season.class);
        System.out.println(es2); // 输出[]
        // 手动添加两个元素
        es2.add(Season.WINTER);
        es2.add(Season.SPRING);
        System.out.println(es2); // 输出[SPRING,WINTER]
        // 以指定枚举值创建EnumSet集合
        EnumSet es3 = EnumSet.of(Season.SUMMER , Season.WINTER);
        System.out.println(es3); // 输出[SUMMER,WINTER]
        EnumSet es4 = EnumSet.range(Season.SUMMER , Season.WINTER);
        System.out.println(es4); // 输出[SUMMER,FALL,WINTER]
        // 新创建的EnumSet集合的元素和es4集合的元素有相同类型,
        // es5的集合元素 + es4集合元素 = Season枚举类的全部枚举值
        EnumSet es5 = EnumSet.complementOf(es4);
        System.out.println(es5); // 输出[SPRING]
    }
}