Java集合(二)Set集合详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_42738043/article/details/85156465

Set集合通用知识:

  • set集合类似一个罐子,程序可以一次把对象丢进set集合,而set集合通常不能记住元素的添加顺序。Set集合与Collection基本相同,没有提供任何额外的方法。实际上Set就是Collection,只是行为略有不同。
  • set集合不允许包含相同元素,Set在每次添加元素(值)的时候(使用add()方法添加),都会把前面的元素和新增的元素进行比较,如果返回的是false,则重复,丢弃;返回的是true,添加到集合当中。
  • 上面介绍的是set集合的通用知识,因此完全适合后面介绍的HashSet、TreeSet和EnumSet三个实现类。只是三个实现类还各有特色。

HashSet类:


import java.util.*;


// 类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);
	}
}

上面程序中向books集合中分别添加了两个A对象 、两个B对象、两个C对象,其中C类重写了equals()方法和hashCode()方法,导致HashSet把两个C对象当成同一对象。

如果两个对象的hashCode()方法返回的hasCode值相同,但他们通过equals()方法比较返回false时更加麻烦:因为两个对象的hasCode()值相同,HashSet将试图把他们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。

重写hashCode()方法的基本规则:

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

  • HashSet是Set接口的典型实现,大多时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能
  • 前面说过,Set集合是不允许重复元素的,否则将会引发各种奇怪的问题。那么HashSet如何判断元素重复呢?
  • 当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值来决定该对象在HashSet中存储位置
  • 如果有两个元素通过equals方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同位置,也就可以添加成功
  • 如果两个元素通过equals为true,并且两个元素的hashCode相等,则这两个元素相等(即重复)
  • 如果要重写保存在HashSet中的对象的equals方法,也要重写hashCode方法,重写前后hashCode返回的结果相等(即保证保存在同一个位置),所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。如果重写了equals方法但不重写hashCode方法,即相同equals结果的两个对象将会被HashSet当作两个元素保存起来,这与我们设计HashSet的初衷不符(元素不重复)
  • 另外如果两个元素哈市Code相等但equals结果不为true,HashSet会将这两个元素保存在同一个位置,并将超过一个的元素以链表方式保存,这将影响HashSet的效率
  • 如果重写了equals方法但没有重写hashCode方法,则HashSet可能无法正常工作,比如下面的例子。

HashSet的特征:

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

LinkedHashSet类:

HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。
LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问的Set里的全部元素是有很好的性能,因为它以链表来维护内部顺序。

public class LinkedHashSetTest
{
	public static void main(String[] args)
	{
		LinkedHashSet books = new LinkedHashSet();
		books.add("a");
		books.add("b");
		System.out.println(books);
		// 删除 a
		books.remove("a");
		// 重新添加 a
		books.add("a");
		System.out.println(books);
	}
}

输出元素的顺序总是与添加的顺序一致:

[a,b]
[b,a]

TreeSet类

TreeSet类的特征

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

  • Object first():返回集合中的第一个元素。 
  • Object last():返回集合中的最末一个元素。 
  • Object lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要是TreeSet的元素)。
  • Object higher(Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是TreeSet的元素)。 
  • SortedSet subSet(fromElement, toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)。 
  • SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成。 
  • SortedSet tailSet(fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。

TreeSet采用红黑树的数据结构对元素进行排序。TreeSet支持两种排序方法:自然排序和定制排序。

  • 自然排序:TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间大小关系,然后将集合元素按升序排列,这种方式就是自然排列。
  • 定制排序:TreeSet借助于Comparator接口的帮助。该接口里包含一个的int compare(T o1, T o2)方法,该方法用于比较o1和o2的大小。

自然排序(在元素中写排序规则)

TreeSet 会调用compareTo方法比较元素大小,然后按升序排序。所以自然排序中的元素对象,都必须实现了Comparable接口,否则会跑出异常。对于TreeSet判断元素是否重复的标准,也是调用元素从Comparable接口继承而来额compareTo方法,如果返回0则是重复元素(两个元素I相等)。Java的常见类都已经实现了Comparable接口,下面举例说明没有实现Comparable存入TreeSet时引发异常的情况。

package collection.Set;
 
import java.util.TreeSet;
 
class Err {
    
}
 
public class TreeSets {
 
    public static void main(String[] args) {
        TreeSet ts =  new TreeSet();
        ts.add(new Err());
        ts.add(new Err());
        System.out.println(ts);
            
    }
}
 


运行程序会抛出如下异常

扫描二维码关注公众号,回复: 4943454 查看本文章
Exception in thread "main" java.lang.ClassCastException: collection.Set.Err cannot be cast to java.lang.Comparable
    at java.util.TreeMap.compare(Unknown Source)
    at java.util.TreeMap.put(Unknown Source)
    at java.util.TreeSet.add(Unknown Source)
    at collection.Set.TreeSets.main(TreeSets.java:13)


将上面的Err类实现Comparable接口之后程序就能正常运行了

class Err implements Comparable {
    @Override
    public int compareTo(Object o) {
        // TODO Auto-generated method stub
        return 0;
    }
}

还有个重要问题是,因为TreeSet会调用元素的compareTo方法,这就要求所有元素的类型都相同,否则也会发生异常。也就是说,TreeSet只允许存入同一类的元素。例如下面这个例子就会抛出类型转换异常

package collection.Set;
 
import java.util.TreeSet;
 
class Err implements Comparable {
    @Override
    public int compareTo(Object o) {
        // TODO Auto-generated method stub
        return 0;
    }
}
 
public class TreeSets {
 
    public static void main(String[] args) {
        TreeSet ts =  new TreeSet();
        ts.add(1);
        ts.add("2");
        System.out.println(ts);
            
    }
}
 


运行结果

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at java.lang.String.compareTo(Unknown Source)
    at java.util.TreeMap.put(Unknown Source)
    at java.util.TreeSet.add(Unknown Source)
    at collection.Set.TreeSets.main(TreeSets.java:18)

定制排序(在集合中写排序规则)

TreeSet还有一种排序就是定制排序,定制排序时候,需要关联一个 Comparator对象,由Comparator提供排序逻辑。下面就是一个使用Lambda表达式代替Comparator对象来提供定制排序的例子。 下面是一个定制排序的列子

package collection.Set;
 
import java.util.Comparator;
import java.util.TreeSet;
 
class M {
    int age;
    public M(int age) {
        this.age = age;
    }
    
    public String toString() {
        return "M[age:" + age + "]";
    }
    
}
 
class MyCommpare implements Comparator{
    
    public int compare(Object o1, Object o2){
        M m1 = (M)o1;
        M m2 = (M)o2;
        return m1.age >  m2.age ? 1 : m1.age < m2.age ? -1 : 0;        
    }
 
}
 
public class TreeSets {
 
    public static void main(String[] args) {
        TreeSet ts =  new TreeSet(new MyCommpare());    
        ts.add(new M(5));
        ts.add(new M(3));
        ts.add(new M(9));
        System.out.println(ts);
            
    }
}


当然将Comparator直接写入TreeSet初始化中也可以。如下。

package collection.Set;
 
import java.util.Comparator;
import java.util.TreeSet;
 
class M {
    int age;
    public M(int age) {
        this.age = age;
    }
    
    public String toString() {
        return "M[age:" + age + "]";
    }
    
}
 
public class TreeSets {
 
    public static void main(String[] args) {
        TreeSet ts =  new TreeSet(new Comparator() {
            public int compare(Object o1, Object 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);
            
    }
}

EnumSet特征

  • EnumSet是一个专为枚举类设计的集合类,EnumSet中所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类的定义顺序来决定集合元素的顺序。
  • EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是当进行批量操作(如调用containsAll 和 retainAll方法)时,如其参数也是EnumSet集合,则该批量操作的执行速度也非常快。 
  • EnumSet集合不允许加入null元素。如果试图插入null元素,EnumSet将抛出 NullPointerException异常。如果仅仅只是试图测试是否出现null元素、或删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。
package collection.Set;
 
import java.util.EnumSet;
 
enum Season
{
    SPRING, SUMMER, FALL, WINTER
}
public class EnumSets {
 
    public static void main(String[] args) {
        //必须用元素对象的类类型来初始化,即Season.class
        EnumSet es1 = EnumSet.allOf(Season.class);
        System.out.println(es1);
        EnumSet es2 = EnumSet.noneOf(Season.class);
        es2.add(Season.WINTER);
        es2.add(Season.SUMMER);
        System.out.println(es2);
        EnumSet es3 = EnumSet.of(Season.WINTER, Season.SUMMER);
        System.out.println(es3);
        EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
        System.out.println(es4);
        EnumSet es5 = EnumSet.complementOf(es4);
        System.out.println(es5);
    }
}

执行结果

[SPRING, SUMMER, FALL, WINTER]
[SUMMER, WINTER]
[SUMMER, WINTER]
[SUMMER, FALL, WINTER]
[SPRING]

各种集合性能分析

HashSet和TreeSet是Set集合中用得最多的I集合。HashSet总是比TreeSet集合性能好,因为HashSet不需要额维护元素的顺序。
LinkedHashSet需要用额外的链表维护元素的插入顺序,因此在插入时性能比HashSet低,但在迭代访问(遍历)时性能更高。因为插入的时候即要计算hashCode又要维护链表,而遍历的时候只需要按链表来访问元素。
EnumSet元素是所有Set元素中性能最好的,但是它只能保存Enum类型的元素
 

猜你喜欢

转载自blog.csdn.net/weixin_42738043/article/details/85156465