Java——Java集合Set

7 篇文章 0 订阅

本文根据疯狂Java讲义(第3版)整理而成,感谢作者李刚老师

如果觉得内容不错的话,推荐大家买一本阅读,绝对物超所值哦

阅读本文前可以先看Java——Java集合概述

二、Set

一、Set概述:

1.Set是一个接口,其父接口是Collection。
2.Set集合类似一个罐子,程序可以依次把多个对象丢进Set集合,而Set集合不能记住元素的添加顺序。
3.Set集合不允许包含相同的元素,如果试图添加两个相同的元素到同一个Set集合中,第二次添加操作会失败并返回false。(两个对象何为相等?a.equals(b) == true说明a 等于 b
4.前两条是Set集合的通用知识,适用于后面介绍的HashSet,TreeSet和EnumSet三个实现类。

二、HashSet

1.HashSet是Set接口的典型实现,大多数使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能(和数组存取不同)。HashSet具有一下特点:

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
  • HashSet不是同步的,如果多个线程同时访问一个HashSet,而且有两个及以上的线程修改集合时,需要通过代码来保证其同步。
  • 集合元素可以是null,但是不能添加两个null。(因为Set集合元素不能重复啊哈哈哈)

2.HashSet添加一个对象O时,HashSet会调用对象O的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet的存储位置。因为hashCode()方法速度快,所以HashSet的存取速度块。如果HashSet再添加对象O时,Hashset会定位到同一个位置,再通过比较发现这两个对象相等,添加就会失败。

3.如果有两个元素通过equals()方法比较返回true(说明这两个对象相等),但它们的hashCode()方法返回值不相等,HashSet将会把它们存在HashSet中不同的位置。这样违反了Set不允许添加相同元素的规则。也就是说,HashSet集合判断两个元素相等的标准是两个对象通过equal()方法比较返回true,并且这两个对象的hashCode()方法返回值也相等。

// 代码同Java疯狂讲义(第三版)P292
class A {
    public boolean equal(Object o) { return true; }
}
class B {
    public int hashCode() { return 1; }
}
class C {
    public int hashCode() { return 2; }
    public boolean equals(Object o) { return true; }
}
public HashSetTest {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        set.add(new A());
        set.add(new A());
        set.add(new B());
        set.add(new B());
        set.add(new C());
        set.add(new C());
        System.out.println(set);
        // 输出 [B@1,B@1,C@2,A@5483cd,A@9931f5]
    }
}

4.从上面的程序可以看出,即使两个A对象通过equals()方法比较返回true,但HashSet依然把它们当成两个对象;即使两个对象的hashCode()方法返回相同值,但HashSet依然把它们当成两个对象。所以如果我们要把一个对象放入HashSet中时,如果需要重写该对象对应类的equals()方法,也应该重写其hashCode()方法。规则是:如果两个对象通过equals()方法比较返回true,这两个对象的hashCode()方法返回值也应该相同。

  • 如果两个对象通过equals()方法比较返回true,但是hashCode()返回值不相等,这将会导致HashSet会把这两个对象保存在集合中不同的位置。虽然二者都会添加成功,但是违反了Set集合不能添加相同元素的规则。

  • 如果两个对象通过equals()方法比较返回false,但是hashCode()返回值相等,这将会导致两个不同的对象添加到相同的位置,通过比较发现这两个对象不相等,HashSet会在这个位置用链式结构将两个对象都保存下来(不能覆盖,因为覆盖后只剩一个对象。如果继续添加定位到此位置但不与该位置其他对象相等的对象,链式结构会越来越长。),但是这会导致集合性能下降。

5.hashCode()方法以及何为hash?hash(也叫做散列,哈希)算法的功能是,它能保证快速查找被检索的对象,hash算法的价值在于速度。我们对比数组,数组根据索引存取元素,所以速度很快,而hash的原理就类似于数组的索引。HashSet使用hash算法,根据对象hashCode值计算出该元素的存储位置,从而快速定位该元素。但是数组索引是连续的,而且数组长度也固定,而HashSet可以自由添加和删除元素。

6.hashCode()方法对于HashSet非常重要(对于后面的HashMap同样重要),下面给出重写hashCode()方法的基本规则:

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

7.如果向HashSet添加可变对象后,尽量不要去修改该集合元素参与计算的hashCode(),equal()的实例变量,否则会导致HashSet无法正确操作这些集合元素。(因为再次添加和删除时会按照这两个方法寻找是否有相同的元素。举例,HashSet添加了一个A对象,A修改后不会改变位置,若再添加一个与A修改前相同的对象,HashSet计算该对象的位置会与A此时的位置相同,所以将会添加到A此时的位置;若删除一个与A修改后相同的对象,通过该对象计算出的位置与A此时的位置并不相同,删除失败)所以向HashSet中添加可变对象时,必须非常小心。

三、LinkedHashSet

1.LinkedHashSet是HashSet的子类。除了具有HashSet的特点之外,它具有以下特点:

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

四、TreeSet

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

  • Comparator comparator(): 如果TreeSet采用定制排序,则该方法返回定制排序所使用的排序状态Comparator;如果采用自然排序,则返回null.
  • Object first(): 返回集合中第一个元素。
  • Object last(): 返回集合中最后一个元素。
  • Object lower(Object e): Java在线API中文文档 - 开源中国
  • Object higher(Object e):
  • SortedSet subSet(Object fromElement, Object toElement):
  • SortedSet headSet(Object toElement):
  • SortedSet tailSet(Object fromElement):

2.TreeSet采用红黑树的数据结构来存储集合元素,保证元素有序。TreeSet支持两种排序方法:自然排序和定制排序。

2.1 自然排序:

  1. 集合要添加的对象所对应的类需要实现Comparable接口并且必须实现其中compareTo方法。该方法返回一个整数值,实现该接口就可以比较大小。
  2. compareTo比较规则:一是相同实例比较,二是obj1.compareTo(obj2),如果该方法返回0,说明二者大小相等;如果返回一个正整数,说明obj1 > obj2;如果返回一个负整数,说明obj1 < obj2。
  3. Java的一些常用类已经实现Compareable接口。例如,BigDecimal,BigInteger以及所有数值类型的包装类,Character,Boolean,String,Date和Time。
  4. 当TreeSet添加一个对象时,TreeSet调用该对象的compareTo(Object o)方法与其他对象比较大小,然后根据红黑树结构找到它的存储位置。
  5. 对于TreeSet集合而言,它判断两个对象是否相等的标准是:两个对象通过compareTo()方法比较是否返回0。当二者compareTo()方法比较返回0时,则通过equals()方法也应该返回true
  6. 总结一句话:如果希望TreeSet正常运作,TreeSet只能添加同一种类型的对象,并且该类型正确地实现了compareTo方法和equals()方法。

2.2 定制排序

  1. TreeSet的自然排序是根据集合元素的大小,TreeSet将他们以升序排列。如果需要实现定制排序,例如降序排列,可以借助Comparator接口的帮助。
  2. 定义一个实现Comparator接口的类,声明TreeSet对象时使用带参数的构造函数:TreeSet(Comparator comparator) ,参数是比较器(实现Comparator接口的类的实例),该构造函数构造了一个新的空 TreeSet并且它根据指定比较器进行排序。
  3. Comparator接口中包含两个方法:(第一个必须实现,第二个不必须)

    • int compare(T o1, T o2): 比较用来排序的两个参数。返回0则相等,判断大小标准根据业务规则定义。
    • boolean equals(Object obj): 指示此 Comparator是否“等于”某个其他对象。
  4. 使用定制排序的TreeSet也只能添加相同类型的对象,否则会引起ClassCastException。

五、EnumSet

1.EnumSet是一个专门为枚举类设计的集合类,EnumSet中的所有元素都必须是是定枚举类型的枚举值,该枚举类型必须在创建EnumSet时显示或隐式的指定。EnumSet的集合元素也是有序地,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。
2.Enum在内部以位向量的形式存储。
3.EnumSet不允许添加null元素,如果试图添加null元素,将会抛出NullPointerException异常。
4.EnumSet没有显示构造器,只能通过类方法来创建EnumSet对象。可以参考JavaAPI文档。

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

HashSet和TreeSet是Set的两个典型实现,到底如何选择HashSet和TreeSet呢?HashSet的性能总是比TreeSet好,(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet.

HashSet还有一个子类:LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet比HashSet要略微慢一点,这是由维护链表所带来的额外开销造成的,但由于有了链表,遍历LinkedHashSet会更快。

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

必须指出的是,Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。通常可以通过Collections工具类的synchronizedSortedSet方法来包装该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。
————李刚老师


参考资料:
1. 疯狂Java讲义(第3版) - 李刚
2. Java在线API中文文档 - 开源中国

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值