Java中的Set集合详解

一、简介

Set集合中的元素是无序的且不可重复, 如果试图把两个相同元素加入同一个Set集合中,则添加操作失败,add()方法返回false,且新元素不会被加入。

二、HashSet类

HashSet底层数据结构是哈希表,因此具有很好的存取和查找性能。
哈希表:一个元素为链表的数组,综合了链表(存储速度快)和数组(查询速度快)的优点。
哈希表的存取原理:
1. 调用对象的hashCode()方法,获得要存储元素的哈希值。
2. 将哈希值与表的长度(即数组的长度)进行求余运算得到一个整数值,该值就是新元素要存放的位置(即是索引值)。

  • 如果索引值对应的位置上没有存储任何元素,则直接将元素存储到该位置上。
  • 如果索引值对应的位置上已经存储了元素,则执行第3步。

3.遍历该位置上的所有旧元素,依次比较每个旧元素的哈希值和新元素的哈希值是否相同。

  • 如果有哈希值相同的旧元素,则执行第4步。
  • 如果没有哈希值相同的旧元素,则执行第5步。

4.比较新元素和旧元素的地址是否相同

如果地址值相同则用新的元素替换老的元素。停止比较。
如果地址值不同,则新元素调用equals方法与旧元素比较内容是否相同。

  • 如果返回true,用新的元素替换老的元素,停止比较。
  • 如果返回false,则回到第3步继续遍历下一个旧元素。

5.说明没有重复,则将新元素存放到该位置上并让新元素记住之前该位置的元素。

HashSet特点:

  • 无序
  • 集合中的元素值可以是null
  • hashSet不是同步的,如果多个线程同时访问一个Set,只要有一个线程修改了Set中的值,就必须进行同步处理,通常通过同步封装这个Set对象来完成同步,如果不存在这样的对象,可以使用Collections.synchronizedSet()方法完成。

实体类:

public class Person {
    String name;
    int age;
    public Person(String name, int age) {
        super();
        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 "Person [name=" + name + ", age=" + age + "]";
    }
}

测试代码:

@Test
public void testHashSet(){
    Person p1 = new Person("钟梅", 25);
    Person p2 = new Person("王兴", 34);
    Person p3 = new Person("张三", 18);
    Person p4 = new Person("李四", 21);
    Person p5 = new Person("李四", 21);
    HashSet<Person> hashSet = new HashSet<>();
    hashSet.add(p1);
    hashSet.add(p2);
    hashSet.add(p3);
    hashSet.add(p4);
    hashSet.add(p5);
    for (Person person : hashSet) {
        System.out.println(person.getName()+ "----------" + person.getAge());
    }
}

输出结果:
在这里插入图片描述
由上可以看到,结果中出现重复元素。在实体类Person中重写hashCode和equals方法:

//判断判断两个对象是否相等,对象是否存在,对象的name和age是否相等
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age &&
            Objects.equals(name, person.name);
}

//返回对象的name和age的hash值
@Override
public int hashCode() {
    return Objects.hash(name, age);
}

重写之后不是判断两个对象hashCode是否相等,而是判断对象的name和age是否同时相等,如果同时相等则判断为同一对象,不能重复出现在集合中。
再次遍历集合,运行结果:
在这里插入图片描述

可以看到重复的元素已经被覆盖,保证了集合中元素的唯一性。

为什么不直接使用数组,而用HashSet呢?
因为数组的索引是连续的而且数组的长度是固定的,无法自由增加数组的长度。而HashSet就不一样了,HashCode表用每个元素的hashCode值来计算其存储位置,从而可以自由增加HashCode的长度,并根据元素的hashCode值来访问元素。而不用一个个遍历索引去访问,这就是它比数组快的原因。

LinkedHashSet类
LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的,也就是说当遍历集合LinkedHashSet集合里的元素时,集合将会按元素的添加顺序来访问集合里的元素。
输出集合里的元素时,元素顺序总是与添加顺序一致。但是LinkedHashSet依然是HashSet,因此它不允许集合重复。

三、TreeSet类

TreeSet可以确保集合元素处于排序状态

内部存储机制
TreeSet内部实现的是红黑树,默认整形排序为从小到大。
在这里插入图片描述
与HashSet集合相比,TreeSet还提供了几个额外方法:

//如果TreeSet采用了定制顺序,则该方法返回定制排序所使用的Comparator,如果TreeSet采用自然排序,则返回null;
Comparator comparator();
//返回集合中的第一个元素;
Object first();
//返回集合中的最后一个元素;
Object last();
//返回指定元素之前的元素。
Object lower(Object e);
//返回指定元素之后的元素。
Object higher(Object e);
//返回此Set的子集合,含头不含尾;
SortedSet subSet(Object fromElement,Object toElement);
//返回此Set的子集,由小于toElement的元素组成;
SortedSet headSet(Object toElement);
//返回此Set的子集,由大于fromElement的元素组成;
SortedSet tailSet(Object fromElement);

用法示例:

@Test
public void testTreeSet(){
    TreeSet<Integer> nums = new TreeSet<>();
    //向集合中添加元素
    nums.add(5);
    nums.add(2);
    nums.add(15);
    nums.add(-4);
    //输出集合,可以看到元素已经处于排序状态
    System.out.println(nums);//[-4, 2, 5, 15]
    System.out.println("集合中的第一个元素:"+nums.first());//集合中的第一个元素:-4
    System.out.println("集合中的最后一个元素:"+nums.last());//集合中的最后一个元素:15
    System.out.println("集合小于4的子集,不包含4:"+nums.headSet(4));//集合小于4的子集,不包含4:[-4, 2]
    System.out.println("集合大于5的子集:"+nums.tailSet(2));//集合大于5的子集:[2, 5, 15]
    System.out.println("集合中大于等于-3,小于4的子集:"+nums.subSet(-3,4));//集合中大于等于-3,小于4的子集:[2]
}

输出结果:
在这里插入图片描述
从上面的运行结果可以看出输出的集合已经按从小到大排好了,但是问题来了,只能从小到大排序吗?如果是字符对象应按该怎样的顺序排序?如果是一个对象又按怎样的顺序排序呢?遵循怎样的排序规则呢?

针对这个问题,TreeSet支持两种排序方法:自然排序和定制排序,在默认情况下,采用的是自然排序。

自然排序
TreeSet会调用集合元素的compareTo(Objec obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这就是自然排序。

拓展:
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类必须实现该方法,实现接口的类就可以比较大小了。当调用一个一个对象调用该方法与另一个对象进行比较时, compareTo(Object obj)如果返回0表示两个对象相等;如果返回正整数则表明obj1大于obj2,如果是负整数则相反。

案例:
实现存储Person类的集合,排序方式,按年龄大小,如果年龄相等,则按name字符串长度,如果长度相等则比较字符。如果name和age都相等则视为同一对象。

public class Person implements Comparable<Person>{
    String name;
    int age;
    public Person(String name, int age) {
        super();
        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 "Person [name=" + name + ", age=" + age + "]";
    }

    @Override
    public int compareTo(Person o) {
        //比较age
        int num=this.age-o.age;
        //如果age相等则比较name长度
        int num1=num==0?this.name.length()-o.name.length():num;
        //如果前两者都相等则比较name字符串
        int num2=num1==0?this.name.compareTo(o.name):num1;
        return num2;
    }
}

测试:

@Test
public void testTreeSet(){
    TreeSet<Person> tree = new TreeSet<>();
    //向集合中添加元素
    tree.add(new Person("孙悟空",16));
    tree.add(new Person("孙悟空",17));
    tree.add(new Person("孙悟空",16));
    tree.add(new Person("唐僧",16));
    tree.add(new Person("沙悟净",23));
    tree.add(new Person("唐僧",30));
    //遍历
    System.out.println(tree);
}

输出:
在这里插入图片描述
从运行结果可以看到满足定义的排序规则。
当把一个对象添加进集合时,集合调用该对象的CompareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树结构中找到它的存储位置。如果两个对象相等则新对象无法加入到集合中。

定制排序
TreeSet的自然排序是根据集合元素的大小,TreeSet将它们以升序排列。如果需要实现定制排序,例如降序排序,则可通过Comparator接口的帮助。该接口里包含一个int compare(T o1,T o2)方法,用于比较o1和o2的大小。由于Comparator是一个函数式接口,因此还可以使用Lambda表达式来代替Comparator子类对象。

@Test
public void testTreeSet2(){
    TreeSet<Integer> nums = new TreeSet<>((a,b)->-(a-b));
    //向集合中添加元素
    nums.add(5);
    nums.add(2);
    nums.add(15);
    nums.add(-4);
    //输出集合,可以看到元素已经处于排序状态
    System.out.println(nums);//[15, 5, 2, -4]
}

以上就是我查看资料后的一些整理,有什么问题请评论区留言。

转载自: https://blog.csdn.net/mashaokang1314/article/details/83721792

  • 18
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值