夯实基础(Java基础学习)——Set集合

目录

  • Set集合
    • HashSet类
    • LinkedHashSet类
    • TreeSet类
      • 1. 自然排序
      • 2. 定制排序
    • EnumSet类
    • 各Set实现类的性能分析

Set集合

Set集合类似于一个罐子,程序可以依次把多个对象“丢进去”Set集合,而Set集合通常不能记住元素的添加顺序。Set集合与Collection基本相同,没有提供任何额外的方法。实际上Set就是Collection,只是行为略有不同(Set不允许包含重复元素)。

Set集合不允许包含相同的元素。如果试图把两个相同的元素添加进入set,则添加操作失败,add() 返回false,且新元素不会被加入。

上面介绍的是Set集合的通用知识,因此完全适用后面介绍的HashSet、TreeSet和EnumSet三个实现类,只是三个实现类还各有特色。

HashSet类

HashSet是set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法存储集合元素当中的元素,因此具有很好的存取权限和查找性能。

HashSet 具有如下的特点。

  • 不能保证元素的排列顺序,顺序可能和添加顺序不同,顺序也有可能发生变化。
  • HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上的线程同时修改了HashSet集合时,则必须通过代码来保证其同步。
  • 集合元素值可以是null

当向HashSet集合元素中存入一个元素的时候,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值。然后根据该hashcode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然添加成功。

也就是说,hashSet集合判断两个元素相等的标准是使用equals方法,并且需要hashCode值也相等。

因此当把一个对象放入hashset中时,如果重写该方法对应类的equals方法,则也应该重写其HashCode方法,规则是:如果两个对象通过equlas()方法比较返回true则它们的hashcode也应该相同。

如果两个对象通过equlas方法比较返回结果为true,但是这两个对象的hashcode值不相同,这将导致hashset会把两个对象保存在不同的位置,从而促使两个对象都可添加成功,这就与Set集合的规则冲突了。

如果两个对象的HashCode()方法返回的hashCode值相同,但它们通过equals方法比较返回false时将会更加麻烦:因为两个对象的hashcode值完全相同,hashSet将会把他们保存在同一位置,但又不行,所以实际上会在这个位置使用链式结构来保存多个对象;而HashSet中两个以上元素具有相同的hashCode值,将会导致性能下降。

如果需要把某个类的对象保存在HashSet集合中,重写这个类的equals方法和hashCode方法时,应该尽量保证两个对象通过equals方法比较返回true时,它们的hashCode方法返回值也应该相等。

HashSet中每个能存储元素的“槽位”(slot)通常称为“桶”(bucket),如果有多个元素的hashCode值相同,但它们通过equlas方法比较返回false ,就需要在一个“桶”里放多个元素,这样会导致性能下降。

前面介绍了hashCode方法对于HashSet的重要性(实际上,对象的HashCode值对于后面的HashMap同样重要),下面给出重写hashCode()方法的基本规则。

  • 当程序运行过程中,同一个对象多次调用hashCode方法应该返回相同的值
  • 当两个对象通过equals方法比较返回true时,这两个对象的hashCode方法应返回相等的值
  • 对象中用作equals方法比较标准的实例变量,都应该用于计算hashCode值。

重写hashCode方法的一般步骤如下:

  1. 把对象内每个有意义的实例变量(即每个参与equals方法比较标准的实例变量计算出来一个int类型的hashCode值)
  2. 用第一步计算出来的多个hashcode值组合计算得到一个HashCode值返回,为了避免直接相加产生偶然相等,可以通过为各实例变量的hashCode 值乘以任意一个质数后再相加。

如果想HashSet中添加一个可变对象后,后面程序修改了该对象的实例变量,则可能导致它与集合中的其他元素相同(即两个对象通过equlas方法比较之后返回结果为true,两个对象的hashCode值也相同),这就导致HashSet中包含两个相同的对象。

import java.util.HashSet;
import java.util.Iterator;

public class App {
    public static void main(String[] args) throws Exception {
        HashSet hs =new HashSet<>();
        hs.add(new R(5));
        hs.add(new R(-3));
        hs.add(new R(-2));
        hs.add(new R(9));
        System.out.println(hs);
        Iterator it = hs.iterator();
        R first = (R) it.next();
        first.count =-3;
        System.out.println(hs);
        hs.remove(new R(-3));
        System.out.println(hs);
    }
}
class R{
    int count ;
    public R(int count){
        this.count=count;
    }
    public String toString(){
        return "R[count:"+ count+"]";
    }
    public boolean equals(Object object){
        if(this == object){
            return true;
        }
        if(object != null && object.getClass() == R.class){
            R r = (R) object;
            return this.count == r.count;
        }
        return false;
    }
    public int hashCode(){
        return this.count;
    }
}

输出结果如下:

[R[count:-2], R[count:-3], R[count:5], R[count:9]]
[R[count:-3], R[count:-3], R[count:5], R[count:9]]
[R[count:-3], R[count:5], R[count:9]]

由上面的程序可以看到,当HashSet中的第一个元素和第二个元素完全相同的时候,这表明两个元素已经完全重复。此时HashSet试图删除count为-3的R对象时,HashSet会计算出该对象的hashCode值,从而找出该对象在集合中的位置。,然后把此处的对象和count 为-3的对象比较,如果相等则删除对象——HashSet只有第二个元素才满足这个条件,因此删除了第二个元素。。至于第一个元素,因为它保存在count值为-3的地方。但是count值为-2。这将导致HashSet不能准确地访问它。

由此可见,当程序把可变对象添加到HashSet中之后,不要再去修改该集合元素中参与计算的hashCode、equals的实例变量,否则将会导致HashSet无法正确操作这些集合元素。

LinkedHashSet类

HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表里维护元素里面的次序,这样使得元素看起来是以插入的顺序来保存的,也就是说,当遍历LinkedHashSet集合的元素时,LinkedHashSet将会按照元素的添加顺序来访问集合里的元素。

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

虽然LinkedHashSet使用了链表记录集合的元素添加顺序,但LinkedHashSet依然是HashSet,因此它依然不允许集合元素重复使用。

TreeSet类

TreeSet是SortedSet接口的实现类,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。与HashSet集合元素相比,TreeSet还提供了如下几个额外的方法。

  • Comparator comparator():如果TreeSet采用了定制排序,则该方法返回定制顺序所使用的的Comparator;如果TreeSet采用了自然排序,则返回null。
  • Object first():返回集合中的第一个元素
  • Object last(): 返回集合中的最后一个元素。
  • Object lower(Object e): 返回集合种位于指定元素元素之前的元素(即小于指定元素的最大元素,参考元素不需要是TreeSet集合中的元素)。
  • Object higher(Object e ):返回集合元素位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是TreeSet集合中的元素)。
  • SortSet subSet(Object fromElement,Object toElement):返回此set的子集合,范围从fromElement(包含)到toElement(不包含)
  • SortedSet headSet(Object toElement):返回此set的子集,由小于toElement的元素组成。
  • SortedSet tailSet(Object fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。

与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。TreeSet支持两种排序方法:自然排序定制排序。默认情况下采用自然排序。

1. 自然排序

TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按照升序排列,这种方式就是自然排序。

Java提供了一个Comparable接口,该接口定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类就可以比较大小。当一个对象调用该方法与另一个对象进行比较的时候:如果方法返回值等于0,那么代表这两个对象相等;如果大于零则说明,this对象比较大;如果小于0说明传入的object比较大。

如果试图将一个对象添加到TreeSet当中的时候,该对象必须实现了Comparable接口,否则系统将会抛出异常。

2. 定制排序

TreeSet的自然排序是根据集合元素的大小,TreeSet将它们以升序排列。如果需要实现定制排序,例如以降序排列,则可以通过Comparator接口的帮助。该接口当中包含一个int Comparator(T o1,T o2)方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1小于o2。

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


import java.util.TreeSet;

public class App {
    public static void main(String[] args) throws Exception {
        //此处的lambda表达式的目标类型时Comparator
        TreeSet ts = new TreeSet<M>((o1,o2) -> 
        {
            M m1 = (M)o1;
            M m2 = (M)o2;
            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);
    }
}
class M{
    int age;
    public M(int age){
        this.age=age;
    }
    public String toString(){
        return "M [age :"+ age +"]";
    }
}

上述代码的输出结果如下

[M [age :9], M [age :5], M [age :-3]]

当通过Comparator对象(或Lambda表达式)来实现TreeSet的定制排序时,依然不可以向TreeSet中添加类型不同的对象,否则会引发异常。

EnumSet类

EnumSet类是一个专门为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式的指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序决定集合元素的顺序。

EnumSet在内部以位向量的形式存储,这种存储形式十分紧凑、高效,因此EnumSet对象占用内存很小。而且运行效率很高。尤其是进行批量操作,若果其参数也是EnumSet集合,则批量操作的执行速度也非常快。

EnumSet集合不允许添加null元素,如果试图添加null元素,系统将会抛出空指针异常。如果只想判断EnumSet是否包含null元素或者删除null元素都不会抛出异常,只是删除操作将会返回false,因为没有任何null元素被删除。

EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过他提供的类方法来创建EnumSet对象。EnumSet类他提供了如下常用的类方法来创建EnumSet对象。

  • EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的EnumSet集合。
  • EnumSet complementOf(EnumSet s):创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合。新EnumSet集合包含原EnumSet集合所不包含的元素。

各Set实现类的性能分析

HashSet和TreeSet是Set的两个经典实现,到底如何选择HashSet和TreeSet呢?HashSet的性能是比TreeSet要好的(特别是常用的添加、查询操作)。

HashSet有一个子类LinkedHashSet,对于普通的的插入查询操作,其要比HashSet要略微慢一点,但是遍历要快一点。

EnumSet是所有的Set当中性能最好的,但是他只能保存同一个枚举类的枚举值作为元素。

你可能感兴趣的:(夯实基础(Java基础学习)——Set集合)