Java讲义笔记(六)——Set集合

一、前言

       Java集合类主要由两个接口派生而出,一个是Collection接口另一个是Map接口。
Collection接口下又派生出Set、List、Queue(Java5之后出现)。
  Set接口下派生出子接口SortedSet(TreeSet类),EnumSet、HashSet(LinkedHashSet)。如下图所示:
在这里插入图片描述
  集合类于数组不同,数组中可以存储基本类型和引用变量,而集合中只能存储引用变量也就是常说的对象。
  Set集合中的元素是无序的、不可重复的。

二、HashSet

 
  HashSet采用哈希算法来存储集合中的元素,因此具有良好的存取和查找性能。
  HashSet存储元素时会先调用存储对象的hashCode()方法,得到hashCode值来决定元素的存储位置。如果两个对象equals()方法的结果为true,而hashCode值不同时,HashSet依然会认为这是两个不同元素,并存储在不同的位置。同样的,如果两个元素hashCode值相同,而equals()为false,HashSet也会认为这是两个不同的元素,并存储在同一位置,而这会导致HashSet的性能下降。总结一下就是,当使用HashSet存储对象时,我们如果需要重写equals()方法,同样也需要重写hashCode()方法。规则如下:两个对象如果相同的话,则equals()方法返回true,hashCode相同。
 重写hashCode()方法的规则如下:

  • 同一个对象多次调用该方法时返回值相同。
  • 当两个对象equals()方法返回true时,hashCode()返回值相同。
  • 对象中在equals()方法中用到的实例变量都应该用于计算hashCode值。

HashSet具有以下特点:

  1. 集合中的元素是无序的。
  2. HashSet本身是线程不安全的,需要使用代码保证其数据的同步。
  3. HashSet可以存储null。
      HashSet在正常添加元素的过程中是不会重复添加的,但我们可以通过后期修改引用变量,达到存储相同对象的目的,这显然是违背设计者的初衷的,我们需要加以警戒。请看如下代码:
public class R {
    
    
    int count;

    public R(int count){
    
    
        this.count=count;
    }

    @Override
    public int hashCode() {
    
    
        return this.count;
    }

    @Override
    public boolean equals(Object obj) {
    
    
        if(this==obj){
    
    
            return true;
        }
        if(obj!=null && obj.getClass()==R.class){
    
    
           return ((R)obj).count==this.count;
        }
        return false;
    }

    @Override
    public String toString() {
    
    
        return "R[count:"+this.count+"]";
    }

    public static void main(String[] args) {
    
    
        HashSet<R> set=new HashSet<R>();
        set.add(new R(1));
        set.add(new R(1));
        set.add(new R(2));
        set.add(new R(3));
        set.add(new R(4));
        //无重复元素
        //输出 :[R[count:1], R[count:2], R[count:3], R[count:4]]
        System.out.println(set);
        //取出第一个元素,更改值
        Iterator it=set.iterator();
        R first=(R)it.next();
        first.count=3;
        //出现重复元素
        //[R[count:3], R[count:2], R[count:3], R[count:4]]
        System.out.println(set);
        //移除count为3的元素
        //[R[count:3], R[count:2], R[count:4]]
        set.remove(new R(3));
        System.out.println(set);
        //是否包含count=3的对象:false
        System.out.println(set.contains(new R(3)));
    }
}

 
  上文提到过我们可以通过手动修改集合元素来达到集合中含重复元素的目的,但是删除操作时又是如何运行的呢?原来,在删除时Java会先获取对象的hashCode去相应的存储位置,随后调用equals()方法比较两个对象是否相同,相同则移除,不同则不做任何操作,这也就解释了为什么删除的元素是集合中第3个而不是第1个。
  代码的最后判断了集合中是否包含count=3的元素,结果同样为false,这是因为,Java在判断是否存在该元素时,同样会调用参数对象的hashCode()方法,然后根据hashCode值去相应的存储位置,通过equals()方法比较两对象是否相同,当同时满足hashCode相同和equals()为 true时,系统才认定包含该元素。事实上,set中剩余的count=3的元素是原先count=2的存储位置,故hashCode值不同。

三、LinkedHashSet

 
  LinkedHashSet实际上是HashSet的子类,它使用链表的数据结构来维持元素的次序,同样的它也是使用hashCode值来决定存储位置,它的性能略低于HashSet,但是维护了元素的次序,这为迭代元素带来了优势。

四、TreeSet

 
  TreeSet实现了SortedSet接口,故其存储的元素都是有序的(随插随排序)。实际上这是通过实现红黑树数据结构来实现的。TreeSet支持两种排序方法:自然排序和定制排序。

  1. 自然排序(默认)
      如名所示,插入的元素是按照从值得大小,从低到高的进行排序的。 它要求存储的元素必须实现Comparable接口,其中包含compareTo(Object obj),否则将会抛出异常。在比较两个元素是否相同时我们往往必须保证这两个元素是同一个类的实例,也就是说TreeSet中存储的对象必须是同一类的,否则也将抛出异常。
      对于TreeSet集合而言,比较两个对象是否相同的唯一标准是compareTo方法的返回值是否为0。如果为0则不会插入相同的元素。由此我们需要注意equals()方法与compareTo方法的结果的一致性。
      与HashSet相同,我们同样可以后期修改其中的元素值,但需要注意的是后期修改后,TreeSet并不会重新对集合元素进行排序,也就是说此时的集合元素可能是无须的。修改后的集合元素无法删除,与修改后的元素相同且未被修改的元素同样无法删除直至重新索引之后。重新索引发生在成功删除一个集合元素之时,但注意重新索引并不是重新排序。
public class R implements Comparable {
    
    
    int count;

    public R(int count){
    
    
        this.count=count;
    }

    @Override
    public int hashCode() {
    
    
        return this.count;
    }

    @Override
    public boolean equals(Object obj) {
    
    
        if(this==obj){
    
    
            return true;
        }
        if(obj!=null && obj.getClass()==R.class){
    
    
           return ((R)obj).count==this.count;
        }
        return false;
    }

    @Override
    public String toString() {
    
    
        return "R[count:"+this.count+"]";
    }

    @Override
    public int compareTo(Object o) {
    
    
        R r =(R)o;
        return this.count>r.count?1:this.count<r.count?-1:0;
    }


    public static void main(String[] args) {
    
    
        TreeSet<R> set=new TreeSet<R>();
        set.add(new R(4));
        set.add(new R(1));
        set.add(new R(1));
        set.add(new R(2));
        //输出[R[count:1], R[count:2], R[count:4]]
        System.out.println(set);
        R first = set.last();
        first.count=-6;
        //输出[R[count:1], R[count:2], R[count:-6]]
        System.out.println(set);
        set.add(new R(3));
        set.add(new R(0));
        //输出[R[count:0], R[count:1], R[count:2], R[count:-6], R[count:3]]
        System.out.println(set);
        set.remove(new R(-6));
        //删除失败,输出[R[count:0], R[count:1], R[count:2], R[count:-6], R[count:3]]
        System.out.println(set);
    }

}
  1. 定制排序
      它可以按照人们的定制规则进行排序如降序。要达成此目的需要通过Comparor接口的帮助,改接口中包含int compare(T o1,T o2),若使用此种方式实现排序,则集合中的元素不必实现Comparable接口。

五、EnumSet

 
  EnumSet中要求的元素必须均为枚举类型的枚举值,顺序按照枚举值的定义顺序进行排序。EnumSet内部以位向量的方式存储,此种形式紧凑高效,故此类Set对象的内存占用少,且效率高。此类Set中不允许插入null,且只允许保存同一个枚举类中的枚举值。同时此类无显式的构造方法。

六、性能分析

EnumSet得益于位向量的存储方式性能最优,但缺点显而易见。HashSet相较于TreeSet在插入和查询元素方面性能更好,而TreeSet由于维护了元素的有序性,性能相对较低,但其得益于链表的存在,遍历数据时更快。Set的三个实现类都是线程不安全的。

猜你喜欢

转载自blog.csdn.net/qq_42451178/article/details/105102462