Collection集合之子接口Set


Set接口简述

Set 接口是 Java 集合框架中的一种接口,它继承自 Collection 接口。Set 是一种无序、不允许重复元素的集合。

Set 接口的主要特点如下:

1.无序性:Set 中的元素没有特定的顺序,不保证元素的存储顺序和插入顺序一致。

2.唯一性:Set 中的元素不允许重复,每个元素在 Set 中只能出现一次。当想要确保集合中的元素唯一性时,可以使用 Set。

3.不可变性:Set 接口并没有提供修改集合元素的方法,即不能直接通过索引修改元素的值。如果想要修改 Set 中的元素,需要先删除原有元素,然后再添加新的元素。

4.常用实现类:Java 提供了几个常用的 Set 实现类,例如 HashSet、TreeSet 和 LinkedHashSet 等。

1.HashSet类

1.Hashset特点:

1.无序性:HashSet 不保证元素的顺序,元素的存储和遍历顺序可能不一致。
2.不允许重复元素:HashSet 不允许存储重复的元素,如果尝试插入重复元素,插入操作将被忽略。
3.允许 null 元素:HashSet 允许存储 null 元素,但只能存储一个 null 元素。

2.Hashset的扩容策略

HashSet 是 Java 中的一个集合类,内部使用哈希表(HashMap)来实现。关于 HashSet 扩容的具体策略如下:

  1. 初始容量:在创建 HashSet 对象时,可以指定初始容量大小。如果没有指定,默认初始容量为 16
  2. 负载因子:负载因子是表示哈希表在扩容之前可以达到多满的一个比例,默认为 0.75。例如,对于初始容量为 16 的HashSet,当元素个数达到 16 * 0.75 = 12 时,就会触发扩容操作。
  3. 扩容:当 HashSet的元素个数超过负载因子所允许的容量时,HashSet会自动进行扩容操作。扩容会创建一个更大容量的哈希表,并将旧表中的元素重新计算哈希码后插入到新表中。
    • 扩容容量为原容量的两倍,即将容量乘以2
    • 扩容后,原来的所有元素会被重新分配到新的位置,这会导致一定的性能开销

总结起来,HashSet 是在元素个数超过初始容量乘以负载因子时进行扩容操作,扩容容量为原容量的两倍。这样的扩容策略可以保持 HashSet 在元素数量逐渐增加时的性能较稳定。

3.Hashset的底层逻辑:

  1. HashSet的底层实现是基于哈希表(HashTable)的数据结构。具体来说,它使用了HashMap作为内部实现。
  2. HashSet通过HashMap来存储元素,其中HashSet的元素被看作是HashMap的键(key),而所有的键的值都被设置为一个常量(PRESENT或者null)。这意味着HashSet实际上是对HashMap的一种封装,只使用了HashMap中的键而没有使用值。
  3. 在HashSet中添加元素时,元素会经过哈希函数计算得到一个哈希码,然后根据哈希码找到对应的桶,如果桶中已经存在相同哈希码的元素,则会检查元素的equals方法是否返回true,如果返回true,则判断元素已经存在,不会重复添加;如果返回false,则将元素作为新的键添加到HashMap中。
  4. 因此,HashSet的底层实现可以认为是一个HashMap,其中元素的值被忽略,只使用HashMap的键来存储元素,并且确保元素在HashSet中的唯一性

4.hashset中哈希桶的存储

在 Java 的 HashSet 中,每个哈希桶(bucket)使用链表或红黑树来存储元素。当多个元素的哈希码映射到同一个桶时,这些元素会以链表形式存储在该桶中。

具体地说,当在 HashSet 中插入一个元素时,会根据元素的哈希码找到对应的桶。如果该桶为空,则直接将元素插入其中。如果该桶不为空,则需要进行以下操作:

  1. 遍历链表:遍历该桶中已存在的元素,比较这些元素与要插入的元素是否相等。如果找到相等的元素,则不进行插入操作,避免元素重复。
  2. 插入新节点:如果链表中不存在与要插入的元素相等的节点,则将新元素以节点的形式插入到链表的末尾。

这样,当多个元素的哈希码映射到同一个桶时,它们会以链表的形式连接在一起,通过链表中的节点进行存储和查找操作。

需要注意的是,为了提高查询效率,当链表中的元素数量达到一定阈值时,HashSet 会将链表转换为红黑树。这是为了在元素数量较大时,仍然能够保持较快的查找速度。当链表转换为红黑树后,与链表相比,查找、插入和删除的时间复杂度将从 O(n) 降低到 O(log n)。

请添加图片描述
图中这个数组0,1,2,3…就代表一个又一个的哈希桶

简单的示例

//下面不同的字符串就有相同的hashCode
 System.out.println("ABCDEa123abc".hashCode());  // 165374702
 System.out.println("ABCDFB123abc".hashCode()); //  165374702

请添加图片描述
可以看到这两个相同hashcode的字符串,但是equals比较之后不相同的话,就形成链表。

当然,我们也可以通过重写添加元素的hashcode()equals()方法来达到自己想要的结果

1. 重写hashCode和equals原则

(1) hashCode

  • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
  • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。
  • 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
    (2) equals
  • 当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是, 根据Object.hashCode()方法,它们仅仅是两个对象。
  • 因此,违反了“相等的对象必须具有相等的散列码”。
  • 结论:复写equals方法的时候一般都需要同时复写hashCode方法。 通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。
2.重写hashCode和equals方法的案例实现

首先我要说的是往hashset里面添加,几乎可以添加任何类型的元素,包括基本类型和对象

  • 1.对于基本类型的元素,Java 会自动将其封装为对应的包装类。例如,可以将 int 类型的元素添加到 HashSet 中,实际上是将其封装为 Integer 类型的对象后进行操作。
  • 2.需要注意的是,如果你想自定义类型的对象能够正确地在 HashSet 中运作,你需要确保重写了该类型的 hashCode() 和 equals() 方法。这样才能正确地判断元素的唯一性,并通过哈希码将其放入正确的桶中。

User类

public class User1![请添加图片描述](https://img-blog.csdnimg.cn/29f9ce3adff345c88ee99fcb87717c8a.png)
 {


    private String name;
    private int age;

    public User1() {
    }

    public User1(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User1{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
//        return super.equals(o);
        if (this == o) return true; //内存地址相同返回true
        if (o == null || getClass() != o.getClass()) return false;

        User1 user = (User1) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == name;
    }

    /**
     *      * 注:**为什么hashCode中,用31这个数子?
     *      *
     *      * - 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
     *      * - 并且31只占用5bits,相乘造成数据溢出的概率较小。
     *      * - 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。 (提高算法效率)
     *      * - 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除! (减少冲突)
     * @return
     */
    @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }


}

测试类

    /**
     * HashSet存入数据的过程
     * 1. 得到对象的hashCode,hashCode%16,决定落到那个槽位(默认16个槽位)Entry[16]
     * 2. 再次添加对象,如果对象的hashCode和第一次添加的相同,执行equals
     * 3. 如果equals返回true,hashCode相同,equals相同,添加失败
     * 4. 如果eques返回false,hashCode相同,形成一个链表
     *
     *
     */
    @Test
    public static void main(String[] args) {
        Set set = new HashSet();
        User1 u1 = new User1("马云",45);
        User1 u2 = new User1("马云",45);
        User1 u3 = new User1("马化腾",40);
        User1 u4 = new User1("马化腾",40);
        User1 u5 = new User1("奥巴马", 50);
        User1 u6 = new User1("马云",45);


        set.add(u1);
        set.add(u2);
        set.add(u3);
        set.add(u4);
        set.add(u5);
        set.add(u6);

        for (Object o : set) {
            System.out.println(o);
        }
    }

在这里插入图片描述

这里没有形成链表,因为重写中我们把equals中名字跟age相同的就认为同一个值就不允许添加。

如果不重写equals方法的话,本来User类的equals比较的就是地址,如果地址相同就会认为是一样的,那么就会把相同姓名和相同年龄的都添加进去就不符合set集合 唯一值的要求

3.哈希桶的链表到红黑树转换过程解析

还是上一段代码,不过鉴于形成红黑树的条件不容易找,我们就把上一段代码的重写equals方法去掉,让他们形成相同的hashcode,但是调用equals比较地址来认为他们是不同的值

改变的代码如下:

  public static void main(String[] args) {
        Set set = new HashSet();
        User1 u1 = new User1("马云",45);
        User1 u2 = new User1("马云",45);
        User1 u3 = new User1("马云",45);
        User1 u4 = new User1("马云",45);
        User1 u5 = new User1("马云",45);
        User1 u6 = new User1("马云",45);
        User1 u7 = new User1("马云",45);
        User1 u8 = new User1("马云",45);
        User1 u9 = new User1("马云",45);
        User1 u10 = new User1("马云",45);
        User1 u11 = new User1("马云",45);
        User1 u12 = new User1("马云",45);


        set.add(u1);
        set.add(u2);
        set.add(u3);
        set.add(u4);
        set.add(u5);
        set.add(u6);
        set.add(u7);
        set.add(u8);
        set.add(u9);
        set.add(u10);
        set.add(u11);
        set.add(u12);

        for (Object o : set) {
            System.out.println(o);
        }
    }

user1


    private String name;
    private int age;

    public User1() {
    }

    public User1(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User1{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
       @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

接下来我们看一下过程
1.最开始:执行完 Set set = new HashSet();语句
在这里插入图片描述

2.执行 set.add(u1);
我们可以看到这个值分到了第五个哈希桶,而且通过第二张图可知没有形成链表
在这里插入图片描述

在这里插入图片描述

3.执行完 set.add(u2);
我们可以看到通过我们自定义hashcode 和equals方法 我们可以让值分到一个哈希桶里

在这里插入图片描述
4.然后我们一直添加值,它就一直往第五个哈希桶里添加,增长链表的长度阈值为8
当第九次添加时候 会对table进行扩容 增加node数量 从 16增加到32
这时候链表还在第五个哈希桶中

在这里插入图片描述

5.第十次添加时候,因为链表长度阈值为8,但是如果想要让哈希桶里的链表形成红黑树,table的长度就至少要64,所以第十次添加的时候,table还会进行第二次扩容正好翻倍到64

每次扩容都要重新排哈希桶来放数据,所以这组暂时的链表就被搬到了第37个哈希桶中
在这里插入图片描述

6.第十一次添加时:因为哈希桶里的链表形成红黑树的条件都已经达到了,所以这里链表就变成了红黑树
这里我认为在table中存储的node节点就会变成红黑树中的根节点,如有错误,欢迎指正
在这里插入图片描述

2.LinkedHashset

  • LinkedHashSet 是 HashSet 的子类
  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
  • LinkedHashSet插入性能略低于 HashSet, 但在迭代访问 Set 里的全部元素时有很好的性能。
  • LinkedHashSet 不允许集合元素重复。
  • LinkedHashSet 底层结构

请添加图片描述

3.Treeset

TreeSet 是 Java 中的一种有序集合,它基于红黑树实现。红黑树是一种自平衡的二叉搜索树,通过对元素进行排序并维护树的平衡,使得所有元素按照特定顺序排列,查询速度比List快TreeSet 是 SortedSet 接口的实现类, TreeSet 可以确保集合元素处于排序状态。而且不能有重复

新增的方法如下:

  • Comparator comparator():返回定制排序器
  • Object first():返回此集合中当前的第一个(最低)元素。
  • Object last():返回此集合中当前的最后一个(最高)元素。
  • Object lower(Object e):返回此集合中的最大元素严格小于给定元素,如果没有这样的元素,则 null 。
  • Object higher(Object e)
  • SortedSet subSet(fromElement, toElement)
  • SortedSet headSet(toElement)
  • SortedSet tailSet(fromElement)

TreeSet的两总排序方式

1. 自然排序

默认情况下, TreeSet 采用自然排序。

  1. 自然排序: TreeSet 会调用集合元素的 compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
  2. 如果试图把一个对象添加到 TreeSet时,则该对象的类必须实现 Comparable接口。
    • 实现 Comparable接口 的类必须实现 compareTo(Objectobj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
  • 0:相等,添加失败
  • -1:小,放到左子树
  • 1:大,放到右子树

Comparable接口的典型实现举例:

  • BigDecimal、 BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
  • Character:按字符的 unicode值来进行比较
  • Boolean: true 对应的包装类实例大于 false 对应的包装类实例
  • String:按字符串中字符的 unicode 值进行比较
  • Date、 Time:后边的时间、日期比前面的时间、日期大

案例(自定义对象的添加):

user类

public class User implements Comparable {

    private String name;
    private int age;

    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //按照姓名从大到小排列,年龄从小到大排列
    @Override
    public int compareTo(Object o) {
        if(o instanceof User){
            User user = (User)o;
//            return -this.name.compareTo(user.name);
//            int compare = -this.name.compareTo(user.name);
            int compare = this.name.compareTo(user.name);
            if(compare != 0){
                return compare;
            }else{
                return Integer.compare(this.age,user.age);
            }
        }else{
            throw new RuntimeException("输入的类型不匹配");
        }

    }

}

测试代码:

public class TreeSetDemo1 {

    @Test
    public void test1() {
        System.out.println("cc".compareTo("ccc"));

//        System.out.println(Integer.compare(3, 2));
    }

    public static void main(String[] args) {
        User u1 = new User("aaa", 35);
        User u2 = new User("bbb", 36);
        User u3 = new User("ccc", 83);
        User u4 = new User("ccc", 30);
        User u5 = new User("ccc", 74);
        User u6 = new User("eee", 39);
        User u7 = new User("fff", 40);
        User u8 = new User("ggg", 40);
        User u9 = new User("ggg", 40);

        TreeSet set = new TreeSet();
        set.add(u1);
        set.add(u2);
        set.add(u3);
        set.add(u4);
        set.add(u5);
        set.add(u6);
        set.add(u7);
        set.add(u8);
        set.add(u9);


        for (Object o : set) {
            System.out.println(o);
        }


    }
}

需要注意的是:

  • 向TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值。
  • 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。否则,让人难以理解。
2.定制排序
  • TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。 需要重写compare(T o1,T o2)方法。
  • 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
  • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
  • 此时, 仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
  • 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。
  • 定制排序器方法compare返回值
  • 0:相等,添加失败
  • -1:小,放到左子树
  • 1:大,放到右子树

案例:

public class TreeSetDemo1 {

    public static void main(String[] args) {
        User u1 = new User("aaa", 35);
        User u2 = new User("bbb", 35);
        User u3 = new User("ccc", 35);
        User u4 = new User("ccc", 30);
        User u5 = new User("ccc", 74);
        User u6 = new User("eee", 39);
        User u7 = new User("fff", 3);
        User u8 = new User("ggg", 2);
        User u9 = new User("ggg", 1);

        //定制排序器,匿名内部类
        Comparator com = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                User u1 = (User) o1;
                User u2 = (User) o2;
//                return -Integer.compare(u1.getAge(), u2.getAge());
                return u1.getName().compareTo(u2.getName());
            }
        };

        TreeSet set = new TreeSet(com);
        set.add(u1);
        set.add(u2);
        set.add(u3);
        set.add(u4);
        set.add(u5);
        set.add(u6);
        set.add(u7);
        set.add(u8);
        set.add(u9);


        for (Object o : set) {
            System.out.println(o);
        }


    }
}


总结

Set与List的差别

Set 和 List 都是 Java 中的集合接口,它们在以下几个方面有所差别:

  1. 元素的顺序:List 是有序集合,可以按照元素添加的顺序访问和获取元素。而 Set 是无序集合,不保证元素的顺序,不能通过索引访问元素。
  2. 元素的唯一性:List 允许存储重复元素,可以通过索引访问和修改相同值的元素。而 Set不允许存储重复元素,每个元素在集合中只能出现一次。
  3. 接口的实现类:Java 提供了多个 List 和 Set 的实现类。常见的 List 实现类有 ArrayListLinkedList等;常见的 Set 实现类有 HashSetTreeSetLinkedHashSet 等。
  4. 查找和插入性能:由于 List 可以根据索引访问元素,因此在遍历和随机访问元素方面具有较好的性能。而 Set在判断元素是否存在和插入新元素方面具有较好的性能,它使用哈希表或平衡树等数据结构来实现高效的查找和插入操作。
  5. Iterator 的顺序:使用 Iterator 迭代器遍历 List 返回的元素是按照插入顺序的。而 Set
    返回的元素顺序可能是任意的,取决于具体的实现类。

选择使用 List 还是 Set 取决于你的具体需求。如果你需要按照插入顺序存储并访问元素,并且允许重复元素出现,则选择 List;如果你需要存储唯一的元素,并且不关心元素的顺序,则选择 Set。

注意,无论使用 List 还是 Set,都应该根据具体的情况选择适合的实现类,以获得更好的性能和功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值