博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

JavaEE - 11集合Collection-Set

Posted on 2020-12-07 22:10  Kingdomer  阅读(110)  评论(0编辑  收藏  举报

JavaEE - 11集合Collection-Set

(5)Set集合

(5.1)Set集合概述 

Set接口: 存储无序、不可重复的数据

  • HashSet: Set主要实现类;线程不安全;可以存储null值
    • LinkedHashSet: HashSet的子类;遍历内部数据时,可以按照添加的顺序遍历
  • TreeSet: 按照添加对象的指定属性进行排序。 
  • 无序性:    不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是按照数据的哈希值。
  • 不可重复性: 保证添加的元素按照equals()判断时,不能返回true。相同的元素只能添加一个。
  • Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。
  • 向Set中添加的数据,其所在的类一定要重写 hashCode() 和 equals()。 对象中用作equals()方法比较的Field,都应该用来计算hashCode值。

(5.2)HashSet类

  • HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。
  • HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能
  • HashSet 特点
    • 不能保证元素的排列顺序。顺序可能与添加顺序不同,顺序也可能发生变化。
    • HashSet 不是线程安全的,如果多个线程同时访问一个HashSet,假设有两个或两个以上线程同时修改了HashSet 集合时,必须通过代码来保证其线程安全。
    • 集合元素可以是 null。
  • HashSet 集合判断两个元素相等的标准: 两个对象 通过 equals() 方法比较相等,hashCode() 方法返回值也相等。
  • 当把一个对象放入 HashSet 中时,如果需要重写该对象对应类的 equals() 方法,也应该重写 hashCode() 方法。
    • 规则是: 如果两个对象通过 equals()方法 比较返回 true,这两个对象的 hashCode 值也应该相同。相等的对象必须具有相等的散列码。
  • 如果试图把两个相同的元素加入到同一个Set集合中,add() 方法返回false, 且新元素不会被加入,添加操作会失败。

(5.2.1)HashSet添加元素过程

  • 向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值;
  • 使用此哈希值通过某种算法计算出在HashSet底层数组中的存放位置(即:索引位置),判断数组此位置上是否已经存在元素。 
    • 如果此位置没有其他元素,则元素a添加成功。--> 成功情况1
    • 如果此位置有其他元素b(或以链表形式存在多个元素),则比较元素a与元素b的hash值:
      • 如果hash值不相同,则元素a 添加成功。  --> 成功情况2
      • 如果hash值相同,则调用元素a所在类的equals()方法: 返回true,元素a添加失败;返回false,元素b添加成功。 -->成功情况3

对于添加成功的情况2和情况3而言: 元素a 与已经存在于指定索引位置上的数据以链表的形式存储。

  • JDK 7: 元素a 放到数组中,指向原来的元素。 元素a 插入到链表的头部。
  • JDK 8: 原来的元素在数组中,指向元素a。    元素a 插入链表的尾部。    七上八下。

(5.2.2)HashSet 底层结构

底层也是数组,初始容量为16,当使用率超过0.75(16*0.75=12),就会扩大容量为原来的2倍,(16,32,64,128......)
HashSet底层: 数组 + 链表的结构。

(5.2.3)hashCode()重写,为什么选择31

  • 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的冲突就越少,查找起来效率也会提高。(减少冲突)
  • 并且31只占用5bits, 相乘造成数据溢出的概率较小。
  • 31可以由 i*31 == (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
  • 31是一个素数,素数作用就是用一个数字来乘以这个素数,最终的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

(5.2.4)修改Set集合实例变量导致元素相同 

如果向 HashSet 中添加一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致它与集合中其他元素相同,导致HashSet 中包含两个相同的对象。

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

Person 类已重写equals()和 hashcode()

    @Test
    public void test5(){
        HashSet set = new HashSet();
        Person p1 = new Person("AA",13);
        Person p2 = new Person("BB",34);
        set.add(p1);
        set.add(p2);

        p1.setName("CC");
        set.remove(p1);          // false
        System.out.println(set); //[Person{name='BB', age=34}, Person{name='CC', age=13}]
        set.add(new Person("CC",13));
        System.out.println(set);  //[Person{name='BB', age=34}, Person{name='CC', age=13}, Person{name='CC', age=13}]
        set.add(new Person("BB",34));
        System.out.println(set);  // [Person{name='BB', age=34}, Person{name='CC', age=13}, Person{name='CC', age=13}]
    }

(5.2.5)在List内去除重复数字,要求实现简单

    public List duplicateList(List list){
        HashSet set = new HashSet();
        set.addAll(list);
        return new ArrayList(set);
    }

    @Test
    public void test4(){
        List list = new ArrayList();
        list.add(new Integer(1));
        list.add(new Integer(2));
        list.add(new Integer(2));
        list.add(new Integer(4));
        list.add(new Integer(4));
        List list2 = duplicateList(list);
        for(Object o : list2){
            System.out.println(o);
        }
    }

(5.3)LinkedHashSet

  • LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据的前一个数据和后一个数据。
    • 在遍历内部数据时,可以按照添加的顺序遍历。
  • 优点: 对于频繁的遍历操作, LinkedHashSet效率高于HashSet。
  • 缺点: LinkedHashSet需要维护元素的插入顺序,性能略低于HashSet。
    @Test
    public void test6(){
        LinkedHashSet books = new LinkedHashSet();

        books.add("疯狂 Java 讲义");
        books.add("轻量级 Java EE 企业应用实战");
        System.out.println(books); // [疯狂 Java 讲义, 轻量级 Java EE 企业应用实战]

        books.remove("疯狂 Java 讲义");
        books.add("疯狂 Java 讲义");
        System.out.println(books); // [轻量级 Java EE 企业应用实战, 疯狂 Java 讲义]
    }

 

(5.4)TreeSet

  • TreeSet 是 SortedSet 接口的实现类。TreeSet 可以确保集合元素处于排序状态。
    • 未实现排序报错: Exception in thread "main" java.lang.ClassCastException: Err cannot be cast to java.lang.Comparable
  • TreeSet 底层使用 红黑树 结构存储数据。有序,查询速度比List快。
  • TreeSet 并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小来进行排序的。
  • 与 HashSet 集合相比, TreeSet 还提供了几个额外的方法
    • Comparator comparator(): 如果TreeSet采用了定制程序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序, 则返回null。
    • Object first():          返回集合中的第一个元素。
    • Object last():           返回集合中的最后一个元素。
    • Object lower(Object e):  返回集合中位于指定元素之前的元素。即小于指定元素的最大元素,参考元素不需要是 TreeSet 集合里的元素。
    • Object higher(Object e): 返回集合中位于指定元素之后的元素。即大于指定元素的最小元素,参考元素不需要是 TreeSet 集合里的元素。
    • SortedSet subSet(Object fromElement, Object toElement): 返回此 Set 的子集合, 范围从 fromElement(包含) 到 toElement(不包括)。
    • SortedSet headSet(Object toElement):   返回此Set的子集,由小于 toElement 的元素组成。
    • SortedSet tailSet(Object fromElement): 返回此Set的子集,由大于或等于 fromElement 的元素组成。
  • TreeSet 支持两种排序方法:自然排序(升序,实现Comparable接口) 和定制排序(Comparator。 在默认情况下, TreeSet 采用自然排序。
  • 往TreeSet添加未实现排序的类的对象时,第一次不会报错,添加第二个对象会报错,会引发 ClassCastException 异常。

(5.4.1)添加元素: 要求是相同类型的对象

    @Test
    public void test2(){
        TreeSet set = new TreeSet();
        set.add(123);
        set.add(456);
//        set.add("aa");  //java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
//        set.add(new Person("Sun",12));
        set.add(332);
        set.add(132);
        System.out.println(set.lower(350)); // 332
        System.out.println(set.higher(300)); //332
        System.out.println(set);  // [123, 132, 332, 456]

        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());  // 123  132 332 456
        }
    }

(5.4.2)自然排序

比较两个对象是否相同的标准: compareTo()返回0,不再是equals()

  • Java 提供了一个 Comparable 接口, 该接口 定义了一个 CompareTo(Object obj)方法,
    • 该方法返回一个整数值, 实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。
  • 当一个对象调用该方法与另一个对象进行比较时, 例如 obj1.compareTo(obj2),
    • 如果该方法返回0,表明两个对象相等;返回正整数,表明 obj1 大于 obj2. 返回负整数,表明 obj1 小于 obj2.
  • Java 的一些常用类已经实现了 Comparable 接口,并提供了比较大小的标准。
    • BigDeciaml、BigInteger以及所有的数值型对应的包装类: 按它们对应的数值大小进行比较。
    • Character:  按字符的 Unicode 值进行比较。
    • Boolean:    true 对应的包装类实例 大于 false 对应的包装类实例。
    • String :    按字符串 中字符的Unicode 值进行比较。
    • Date、Time: 后面的时间、日期比 前面的日期、时间大。
  • 当需要把一个对象放入 TreeSet 中, 重写该对象对应类的equals() 方法时,保证该方法与 CompareTo(Object obj) 方法有一致的结果。
    • equals() 返回 true, CompareTo(Object obj) 返回 0。
    • CompareTo(Object obj) 返回 0, equals() 返回 false, TreeSet 不会让第二个元素添加进去,与 Set 集合的规则 产生冲突。
  • 如果向 TreeSet 中添加 一个可变对象后, 并且后面程序 修改了 该可变对象的实例变量,将导致 它与其他对象的大小顺序发生改变。
    • 但 TreeSet 不会再次 调整 它们的顺序, 甚至可能导致 TreeSet 中保存的两个对象通过 CompareTo(Object obj) 方法返回0。
  • 一旦改变了 TreeSet 集合里可变元素的实例变量, 当再试图删除该对象时, TreeSet会删除失败,
    • 甚至 集合中原有的、实例变量没被修改 但与 修改后元素相等的元素也无法删除。
public class Person implements Comparable {
    ......
    @Override
    public int compareTo(Object o){  // 按照姓名从小到大排列
        if(o instanceof  Person){
            Person p = (Person) o;
            return -this.name.compareTo(p.name);
        }else {
            throw new RuntimeException("输入的类型不匹配");
        }
    }
}
    @Test
    public void test3(){
        TreeSet set = new TreeSet();
        set.add(new Person("Aaaa",12));
        set.add(new Person("Abbb",12));
        set.add(new Person("Efrd",13));
        set.add(new Person("Greg",32));
        set.add(new Person("Dfgr",52));
        //[Person{name='Greg', age=32}, Person{name='Efrd', age=13}, Person{name='Dfgr', age=52}, Person{name='Abbb', age=12}, Person{name='Aaaa', age=12}]
        System.out.println(set);
    }

(5.4.3)定制排序

比较两个对象是否相同的标准: compare()返回0,不再是equals()。

定制排序依然不可以向 TreeSet 中添加不同类型的对象。否则引发 ClassCastException 异常。

 @Test
    public void  test4(){
        Comparator com = new Comparator() {
            // 按照年龄从小到大
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof Person && o2 instanceof Person){
                    Person p1 = (Person) o1;
                    Person p2 = (Person) o2;
                    return Integer.compare(p1.getAge(), p2.getAge());
                }else {
                    throw new RuntimeException("输入的类型不匹配");
                }
            }
        };
        TreeSet set = new TreeSet(com);
        set.add(new Person("Aaaa",12));
        set.add(new Person("Abbb",12));
        set.add(new Person("Efrd",13));
        set.add(new Person("Greg",32));
        set.add(new Person("Dfgr",52));
        // [Person{name='Aaaa', age=12}, Person{name='Efrd', age=13}, Person{name='Greg', age=32}, Person{name='Dfgr', age=52}]
        System.out.println(set);
    }


(5.5)EnumSet

  • EnumSet是一个专为枚举类设计的集合类,EnumSet 中所有元素都必须是指定枚举类型的枚举值。该枚举类型在创建 EnumSet 时显式或隐式地指定
  • EnumSet的集合元素也是有序的, EnumSet 以枚举值在Enum类内的定义顺序来决定集合元素的顺序
  • EnumSet在内部以位向量的形式存储, 这种存储形式非常紧凑、高效,EnumSet对象占用内存很小, 而且运行效率很好。
    • 尤其是进行批量操作( 调用 containsAll() 和 retainAll() 方法)时,如果参数也是 EnumSet 集合,则批量操作的执行速度也非常快。
  • EnumSet集合不允许加入null元素, 如果试图插入 null 元素, EnumSet 将抛出 NullPointerException 异常。
  • 如果只是想判断EnumSet是否包含 null 元素或试图 删除 null 元素 都不会抛出异常,只是删除操作将返回 false, 因为没有任何null 元素被删除。
  • EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法 来创建 EnumSet 对象。

(5.5.1)方法

  • EnumSet allOf(Class elementType): 创建一个包含指定枚举类里所有枚举值的EnumSet集合。
  • EnumSet complementOf(EnumSet s):  创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,
    • 新EnumSet集合包含原EnumSet集合所不包含的、此枚举类剩下的枚举值。
    • (即 新EnumSet集合和原EnumSet集合的集合元素加起来就是该枚举类的所有枚举值) <差集>。
  • EnumSet copyOf(Collection c):     使用一个普通集合来创建 EnumSet 集合。
  • EnumSet copyOf(EnumSet s):        创建一个与指定 EnumSet 具有相同元素类型、相同集合元素的 EnumSet 集合。
  • EnumSet noneOf(Class elementType): 创建一个元素类型为指定枚举类型的 空 EnumSet.
  • EnumSet of(E first, E... rest):   创建一个包含一个或多个枚举值的 EnumSet集合,传入的多个枚举值 必须属于 同一个枚举类。
  • EnumSet range(E from, E to):      创建一个包含从 from 枚举值 到 to 枚举值 范围内 所有枚举值的 EnumSet 集合。
        EnumSet es1 = EnumSet.allOf(Season.class);
        System.out.println(es1);         // [SPRING, SUMMER, AUTUMN, WINTER]
        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 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, AUTUMN, WINTER]

        EnumSet es5 = EnumSet.complementOf(es4);
        System.out.println(es5);         // [SPRING]
        Collection c = new HashSet();
        c.clear();
        c.add(Season.AUTUMN);
        c.add(Season.SPRING);
        // 复制 Collection 集合中的所有元素 来创建 EnumSet 集合
        EnumSet enumSet = EnumSet.copyOf(c);    
        System.out.println(enumSet);            // [SPRING, AUTUMN]

        c.add("疯狂 Java 讲义");
        c.add("疯狂 IOS 讲义");
        System.out.println(c);                  // [AUTUMN, SPRING, 疯狂 IOS 讲义, 疯狂 Java 讲义]
        // 下面代码出现异常, 因为 c 集合中的元素 并不是 全部都是 枚举值
        enumSet = EnumSet.copyOf(c);      
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Enum
    at java.util.RegularEnumSet.add(RegularEnumSet.java:36)
    at java.util.EnumSet.copyOf(EnumSet.java:179)
    at SetTest.test8(SetTest.java:137)

 

(5.6)Set实现类比较分析

  • HashSet 和 TreeSet 是 Set 的两个典型实现。
    • HashSet 的性能总是比 TreeSet 好(特别是 最常用的添加、查询元素等操作),因为 TreeSet 需要额外的红黑树算法来维护集合元素的次序。
    • 只有当需要 一个保持顺序的Set时, 才应该使用TreeSet, 否则都应该使用 HashSet
  • HashSet 还有一个子类: LinkedHashSet,对于普通的插入、删除操作, LinkedHashSet 比 HashSet 要略微慢一点。
    • 这是由维护 链表所带来的额外开销造成的。由于有了链表, 遍历 LinkedHashSet 会更快。
  • EnumSet 是所有 Set 实现类 中性能最好的, 但它只能保存同一个枚举类的枚举值作为集合元素。
  • Set 的三个实现类 HashSet、TreeSet、EnumSet都是线程不安全的。
    • 如果有多个线程同时访问 一个Set集合, 有超过一个线程修改了Set集合,则必须手动保证该 Set 集合的同步性。
    • 通常可以通过 Collections 工具类的 synchronizedSortedSet 方法来“包装”该 Set 集合。
      • 此操作最好在创建时进行, 以防止 对Set集合的意外非同步访问。
      • SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));