Java集合总结(二)--Set集合

时间:2021-10-02 17:56:58

Set集合

Set集合保存的元素是没有顺序、且不能重复的。它有三个实现类:HashSet、TreeSet、EnumSet

1、HashSet

HashSet按照Hash算法来存储集合中的元素,因此具有良好的存取和查找性能

特点:

  • 不保证元素的排列顺序

  • HashSet不是同步的,所以是线程不安全的

  • 集合元素可以是null

存储的特性:

  • 当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法,等到该对象的hashCode值,根据这个值来决定该对象在HashSet中存储的位置。

  • 如果两个元素通过equals()方法返回true,但它们hashCode()方法返回值不同,HashSet会把它们存储在不同的位置,一样可以添加成功。


HashSet判断两元素相等的标准:

两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相同


tips:

1、如果把一个对象放入HashSet中时,如果需要重写该对象对应类的equals()方法时,则也应该重写其hash Code()方法。

具体规则:当两个对象通过equals()方法比较返回true,则这两个对象的hashCode值也应该相同。

2、如果两个对象通过equals()方法比较返回true,但hashCode()方法false,则HashSet会把这两个对象保存在不同的位置,这两个对象都可以添加成功。

3、如果两个对象通过equals()方法比较返回false,但hashCode()方法true,则HashSet会把这两个对象保存在同一个位置,在这个位置通过链式结构来保存多个对象。

因为HashSet访问集合元素时也是根据元素的hashCode值来快速定位,因此当HashSet中有两个以上的元素具有相同的hashCode值,将会导致性能的下降。

4、当从HashSet中访问元素时,HashSet先计算该元素的hashCode值(即调用该对象的hashCode()方法的返回值),然后直接到该hashCode值对应的位置去取出该元素,这就是HashSet速度很快的原因。

2、LinkedHashSet

定义:

1、LinkedHashSet也是根据元素的hashCode值来决定元素存储位置

2、但它同时使用链表维护元素的次序,从而可以保证元素是按照插入顺序进行保存的。

即:当遍历LinkedHashSet时,LinkedHashSet会按照元素的添加顺序来访问集合里的元素。

3、也是一个Set,因此也不允许元素重复

性能:

因为LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet,但在迭代访问Set里的全部元素时将有很好的性能,因为它是以链表来维护内部顺序的。

3、TreeSet类

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

TreeSet相比于HashSet多了如下几个额外的方法:
Java集合总结(二)--Set集合

TreeSet类集合元素排序方式:

一般而言TreeSet是根据元素实际值的大小来进行排序的,TreeSet采用红黑树的数据结构来存储集合元素。TreeSet支持两种排序方法:自然排序定制排序

1、自然排序

① 原理:

TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小,然后将集合元素按照升序进行排列。

② 要求:

1、因为compareTo(Object obj)方法是Comparable接口中定义的方法,所以如果要把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口,否则TreeSet无法进行比较,从而会抛出异常ClassCastException

2、因为只有相同类的实例才会比较大小,这就要求向TreeSet中添加的应该是同一个类的对象

③判断元素是否相等:

TreeSet判断两个元素是否相等的唯一标准是:两个对象通compareTo(Object obj)方法比较是否返回0,如果返回0,则TreeSet会认为它们相等,否则就认为它们不相等。

④ 注意点

1、当把一个对象放入TreeSet中时,如果重写了该对象的 equals() 方法时,也应该保证该方法和 compareTo(Object obj)方法有一致的返回结果。规则 equals() 返回true时,compareTo(Object obj) 返回0.

2、 如果向TreeSet中添加一个可变对象后,并且程序修改了该可变对象的实例变量,这将导致与其他对象的大小顺序发生了改变,但TreeSet不会再重新调整它们的顺序。甚至有可能导致TreeSet中两个对象通过compareTo(Object obj)比较后返回0。

如果一旦改变了TreeSet集合里可变元素的实例变量,当试图再删除该对象时,TreeSet也会从删除失败(甚至集合中原有的、实例变量没有被修改但与修改后元素相等的元素也无法删除)


2、定制排序

① 原理:

1、通过Comparator接口来实现,该接口中包含一个 compare(T o1,T o2
)
方法,该方法用于比较o1和o2的大小。

2、实现定制排序,需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由Comparator对象负责集合元素的逻辑排序。


代码示例:

/*自定义类M*/
class M
{
    int age;
    public M(int age)
    {
        this.age = age;
    }
    public String toString()
    {
        return "M[age:" + age + "]";
    }
}

/*自定义的排序规则*/
public class comparatorM implements Comparator{
    @Override
    public int compare(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;
    }
}

public class TreeSetTest4 {
    public static void main(String[] args)
    {
        // 提供一个Comparator对象与该TreeSet集合关联
        TreeSet ts = new TreeSet(new comparatorM());
        ts.add(new M(5));
        ts.add(new M(-3));
        ts.add(new M(9));
        System.out.println(ts);
    }
}



②定制排序判断元素是否相等:

通过Comparator比较两个元素是否返回0,如果返回了0,则TreeSet是不会把第二个元素添加到集合当中。

4、EnumSet类

定义:

1、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]
    }
}



2、EnumSet集合元素也是有顺序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序

3、EnumSet在内部以位向量的形式存储,该存储形式非常紧凑、高效,因此EnumSet对象占用内存小,且运行效率很好,尤其是进行批量操作(如:containsAll()和retainAll()方法时)

4、EnumSet不允许加入null元素,如果试图插入,会报空指针异常

5、当复制Collection集合中所有的元素来创建新的EnumSet集合时,要求Collection集合中所有的元素必须是同一个枚举类型的枚举值。

5、各Set实现类的性能分析

1、HashSet的性能总是好于TreeSet(特别是常用的添加、查找等操作),因为TreeSet需要额外的红黑树算法来维持集合元素的顺序,因此当需要一个保持排序的Set时,才应该使用TreeSet,否则使用HashSet

2、对于普通的插入、删除操作,LinkedHashSet要比HashSet慢一点,因为是由维护链表所造成的,但遍历时LinkedHashSet会快。

3、EnumSet是实现类中性能最好的,但它因为只能保存一个枚举类型当中的枚举值作为集合元素,所以有局限性

4、Set的三个实现类都是线程不安全的,为了保证线程安全,必须手动保证该Set集合的同步性,通常是通过Collections工具类的synchronizedSortedSet方法来“包装”该Set集合,此操作最好在创建时进行,以防止对Set集合的意外非同步操作,如:

SortedSet s = Collections.synchronizedSortedSet(new TreeSet<>());