java核心知识之集合详解二(Set集合)

Set集合

java的Set集合只能存放无序的,不能重复的数据, Set集合与Collection的用法基本类似,可以这么说,Set集合就是Collection(但Set集合不能存放相同的元素,如果使用add添加相同的元素,add会返回false,且添加的元素也没有添加进去)

HashSet类

HashSet 是 Set 接口的典型实现,HashSet 按 Hash算法来存储集合中的元素,因此具有很好的存取和查找性能 。

HashSet具备的特点

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

HashSet的存储流程

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

HashSet 集合判断两个元素相等的标准是两个对象通过 equals()方法比较相等,并且两个对象的 hashCode()方法返回值也相等。

实例代码

import java.util.HashSet;

/**
 * @author: 随风飘的云
 * @date 2022/03/18 23:28
 */

class animal{
    
    
    public boolean equals(Object obj){
    
    
        return true;
    }
}

class Cat{
    
    
    public int hashCode(){
    
    
        return 1;
    }
}

class Dog{
    
    

    public boolean equals(Object obj){
    
    
        return true;
    }
    public int hashCode(){
    
    
        return 2;
    }

}
public class HashSetTest {
    
    
    public static void main(String[] args) {
    
    
        HashSet set =  new HashSet();
        set.add(new animal());
        set.add(new animal());
        set.add(new Cat());
        set.add(new Cat());
        set.add(new Dog());
        set.add(new Dog());
       	set.stream().forEach(System.out::println);
    }
}

结果:
在这里插入图片描述
结果分析

首先animal类重写的equals方法,该方法永远返回true,Cat类中重写了hasCode方法,它永远返回1,Dog重写了hashCode方法和equals方法,分别返回2和true,程序中一共分别添加了两个animal类,Cat类,Dog类,但是输出的除了只有一个Dog类,其他的都是两个输出的。只有一个解析:生成的两个animal和Cat都被HashSet集合当做两个不同的类了,而生成的两个Dog类只被当做一个类。因此可以得出HashSet集合判断两个不同的类是通过hashCode方法和equals方法同时进行的。

HashSet使用特点

当把一个对象放入 HashSet 中时, 如果需要重写该对象对应类的 equals()方法 ,则也应该重写其 hashCode()方法 。 规则是 : 如果两个对象通过 equals()方法比较返回 true,这两个对象的 hashCode 值也应该相同 。

不重写可能性后果一
如果说没有重写这两个方法,假如有两个对象通过equals方法判断得出返回true,但是他们的hashCode方法返回的hash值是不同的,HashSet集合认定这两个对象是不一样的,把他们存放进去,这就与Set集合的不可重复性相反

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

重写hashCode基本规则

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

计算方式
1、把对象内每个有意义的实例变量( 即每个参与 equals()方法比较标准的实例变量) 计算出一个int 类型的 hashCode 值
在这里插入图片描述
2、用第 1 步计算出来的多个 hashCode 值组合计算出 一个 hashCode 值返回 。

return f1 . hashCode() + (int)f2;

3、为了避免直接相加产生偶然相等(两个对象的f1,f2实例变量并不相等,但它们的 hashCode 的和恰好相等),可以通过为各实例变量 的 hashCode 值乘以任意一个质数后再相加。

return f1.hashCode()*19 + (int)f2 * 31;

hashCode的内置的实例代码

// 来源于AbstractSet
public int hashCode() {
    
    
        int h = 0;
        Iterator<E> i = iterator();
        while (i.hasNext()) {
    
    
            E obj = i.next();
            if (obj != null)
                h += obj.hashCode();
        }
        return h;
    }

实例代码

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

/**
 * @author: 随风飘的云
 * @date 2022/03/18 23:53
 */
class R
{
    
    
    int count;
    public R(int count)
    {
    
    
        this.count = count;
    }
    public String toString()
    {
    
    
        return "R[count:" + count + "]";
    }
    public boolean equals(Object obj)
    {
    
    
        if(this == obj)
            return true;
        if (obj != null && obj.getClass() == R.class)
        {
    
    
            R r = (R)obj;
            return this.count == r.count;
        }
        return false;
    }
    public int hashCode()
    {
    
    
        return this.count;
    }
}
public class HashSetTest2 {
    
    
    public static void main(String[] args) {
    
    
        HashSet hs = new HashSet();
        hs.add(new R(5));
        hs.add(new R(-3));
        hs.add(new R(9));
        hs.add(new R(-2));
        // 打印HashSet集合,集合元素没有重复
        System.out.println(hs);
        // 取出第一个元素
        Iterator it = hs.iterator();
        R first = (R)it.next();
        // 为第一个元素的count实例变量赋值
        first.count = -3;     // ①
        // 再次输出HashSet集合,集合元素有重复元素
        System.out.println(hs);
        // 删除count为-3的R对象
        hs.remove(new R(-3));    // ②
        // 可以看到被删除了一个R元素
        System.out.println(hs);
        System.out.println("hs是否包含count为-3的R对象?"
                + hs.contains(new R(-3))); // 输出false
        System.out.println("hs是否包含count为-2的R对象?"
                + hs.contains(new R(-2))); // 输出false
    }
}

结果:
在这里插入图片描述

LinkedHashSet类

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

import java.util.LinkedHashSet;

/**
 * @author: 随风飘的云
 * @date 2022/03/19 0:23
 */
public class LinkedHashSetTest {
    
    
    public static void main(String[] args) {
    
    
        LinkedHashSet books = new LinkedHashSet();
        books.add("哈哈哈");
        books.add("我你他");
        System.out.println(books);
        // 删除
        books.remove("哈哈哈");
        // 添加
        books.add("哈哈哈");
        System.out.println(books);
    }
}

结果:
在这里插入图片描述

TreeSet 类

TreeSet 是 SortedSet 接口的实现类,可以对Set集合中的元素进行排序,是不同步的。TreeSet可以确保集合元素处于排序状态 。

TreeSet的Api方法

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 tomElement): 返回此 Set 的子集 ,由大于或等于tomElement 的元素组成 。

实例代码

import java.util.TreeSet;

/**
 * @author: 随风飘的云
 * @date 2022/03/19 1:05
 */
public class TreeSetTest {
    
    
    public static void main(String[] args) {
    
    
        TreeSet nums = new TreeSet();
        // 向TreeSet中添加四个Integer对象
        nums.add(5);
        nums.add(2);
        nums.add(10);
        nums.add(-9);
        // 输出集合元素,看到集合元素已经处于排序状态
        System.out.println(nums);
        // 输出集合里的第一个元素
        System.out.println(nums.first()); // 输出-9
        // 输出集合里的最后一个元素
        System.out.println(nums.last());  // 输出10
        // 返回小于4的子集,不包含4
        System.out.println(nums.headSet(4)); // 输出[-9, 2]
        // 返回大于5的子集,如果Set中包含5,子集中还包含5
        System.out.println(nums.tailSet(5)); // 输出 [5, 10]
        // 返回大于等于-3,小于4的子集。
        System.out.println(nums.subSet(-3 , 4)); // 输出[2]
    }
}

结果:
在这里插入图片描述
结果分析:
从上面的程序中可以看出,TreeSet不是根据元素的插入顺序来排序的,而是根据元素的实际大小来排序的。HashSet集合是根据hash算法来决定元素的存储位置不同,而TreeSet是根据红黑树的数据结构来存储集合元素。TreeSet集合支持两种排序规则,自然排序和定制排序,默认情况下TreeSet支持自然排序。

TreeSet的排序之自然排序

TreeSet 会调用集合元素的 compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列, 这种方式就是自然排序。
当一个对象调用该方法与另一个对象进行比较时,例如 obj1.compareTo(obj2) ,如果该方法返回 0 ,则表明这两个对象相等 ;如果该方法返回一个正整数 ,则表 明 obj1 大于obj2;如果该方法返回一个负整数 ,则表明 obj1小于 obj2 。(要想实现自然排序,必须要实现Comparable接口,否则就会引发运行时异常-ClassCastException)

实例代码:

import java.util.TreeSet;

/**
 * @author: 随风飘的云
 * @date 2022/03/19 1:14
 */

class Pet{
    
    

}

public class TreeSetErrTest {
    
    
    public static void main(String[] args) {
    
    
        TreeSet set = new TreeSet();
        // set.add(new Pet());(都会报错)
        set.add(new Dog());
        set.add(new Pet());
    }
}

结果:
在这里插入图片描述
注意:
类在实现 compareTo(Object obj)方法时,都需要将被 比较对象 obj 强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小 。 否则也会报ClassCastException异常

实例代码:

import java.util.TreeSet;

/**
 * @author: 随风飘的云
 * @date 2022/03/19 1:14
 */

public class TreeSetErrTest {
    
    
    public static void main(String[] args) {
    
    
        TreeSet set = new TreeSet();
        set.add(new String());
        set.add(new int[]{
    
    0});
    }
}

结果:
在这里插入图片描述
TreeSet集合添加元素流程

1、当把一个对象加入 TreeSet 集合中时, TreeSet 调用该对象的 compareTo(Object obj)方法与容器中的其他对象比较大小。
2、根据红黑树结构找到它的存储位置 。 如果两个对象通过 compareTo(Object obj)方法比较相等,新对象将无法添加到 TreeSet 集合中 。

实例代码:

import java.util.TreeSet;

/**
 * @author: 随风飘的云
 * @date 2022/03/19 1:24
 */
class Z implements Comparable {
    
    
    int age;
    public Z(int age)
    {
    
    
        this.age = age;
    }
    // 重写equals()方法,总是返回true
    public boolean equals(Object obj)
    {
    
    
        return true;
    }
    // 重写了compareTo(Object obj)方法,总是返回1
    public int compareTo(Object obj)
    {
    
    
        return 1;
    }
}
public class TreeSetTest2 {
    
    
    public static void main(String[] args) {
    
    
        TreeSet set = new TreeSet();
        Z z1 = new Z(6);
        set.add(z1);
        // 第二次添加同一个对象,输出true,表明添加成功
        System.out.println(set.add(z1));    //①
        // 下面输出set集合,将看到有两个元素
        System.out.println(set);
        // 修改set集合的第一个元素的age变量
        ((Z)(set.first())).age = 9;
        // 输出set集合的最后一个元素的age变量,将看到也变成了9
        System.out.println(((Z)(set.last())).age);
    }
}

结果:
在这里插入图片描述
注意:
如果两个对象通过 equalsO方法比较返回 true 时,这两个对象通过 compareTo(Object obj)方法比较应返回 0 。

TreeSet的排序之定制排序

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

实例代码:

import java.util.TreeSet;

/**
 * @author: 随风飘的云
 * @date 2022/03/19 1:31
 */
class M{
    
    
    int age;
    public M(int age) {
    
    
        this.age = age;
    }
    public String toString() {
    
    
        return "M [age:" + age + "]";
    }
}
public class TreeSetTest4 {
    
    
    public static void main(String[] args) {
    
    
        // 此处Lambda表达式的目标类型是Comparator
        // 重写了Comparator中的compare方法。
        TreeSet ts = new TreeSet((o1 , o2) ->{
    
    
            M m1 = (M)o1;
            M m2 = (M)o2;
            // 根据M对象的age属性来决定大小,age越大,M对象反而越小
            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 集合不允许加入 null 元素,如果EnumSet集合想判断是否包含null元素或者是试图删除null元素都不会报出异常,而且删除null操作会返回false。

EnumSet类提供的API方法

EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的 EnumSet 集合 。

EnumSet complementOf(EnumSet s): 创建一个其元素类型与指定 EnumSet 里元素类型相同的

EnumSet 集合,新 Enu日lSet 集合包含原 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 集合 。

实例代码:

import java.util.EnumSet;

/**
 * @author: 随风飘的云
 * @date 2022/03/19 1:42
 */

enum TimeClass{
    
    
    YEAR,
    MONTH,
    DAY,
    HOUR,
    MINUTE,
    SECOND;
}
public class EnumSetTest {
    
    
    public static void main(String[] args) {
    
    
        //创建一个 EnumSet 集合 , 集合元素就是 Season 枚举类的全部枚举值
        EnumSet set = EnumSet.allOf(TimeClass.class);
        System.out.println(set);

        // 创建空集合,指定其集合元素是 Season 类的枚举值
        EnumSet set1 = EnumSet.noneOf(TimeClass.class);
        System.out.println(set1);
        set1.add(TimeClass.YEAR);
        set1.add(TimeClass.MONTH);
        set1.add(TimeClass.DAY);
        System.out.println(set1);

        //以指定枚举值创建 EnumSet 集合
        EnumSet set2 = EnumSet.of(TimeClass.YEAR, TimeClass.MONTH, TimeClass.DAY);
        System.out.println(set2);

        // 创建一个包含从 from 枚举值到 to 枚举值范围 内所有枚举值的EnumSet 集合 。
        EnumSet set3 = EnumSet.range(TimeClass.YEAR, TimeClass.SECOND);
        System.out.println(set3);

        // set2+set4 = 枚举类的所有元素
        EnumSet set4 = EnumSet.complementOf(set2);
        System.out.println(set4);
    }
}

结果:
在这里插入图片描述

各种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 工具类的 syncbronizedSortedSet 方法来 "包装"该 Set 集合。 此操作最好在创建时进行 , 以防止对 Set 集合的意外非同步访问 。

猜你喜欢

转载自blog.csdn.net/m0_46198325/article/details/123586417
今日推荐