五、Java集合类(二)Set

今天的博客主题

      基础篇 --》常用类 --》Java集合类(二)Set


在上一篇介绍了Java集合类里的List集合,这篇就说一下Set集合。

 

Set

public interface Set<E> extends Collection<E> {}

 

Set是一个接口继承了Collection接口。

Set的一个体系结构大概是这样子的

通过源码里的类注释上得知,Set是一种无序,不可重复的集合。

Set接口里提供了许多操作Set集合的方法,主要看下具体实现吧。

 

HashSet

 

public class HashSet<E> extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{}

HashSet继承了AbstractSet,实现了Set、Cloneable和Serializable接口。

AbstractSet继承了AbstractCollection类,实现了Set接口。

特点:

  • 底层是有hash算法实现的,具有很好的存取查找性能。
  • 元素不能重复
  • 元素无序
  • 允许插入null值
  • 线程不安全。(若两个线程同时操作,需要通过代码来实现同步)

HashSet底层数据结构实现是有HashMap来完成的,属于哈希表结构。

新增元素相当于HashMap的key,而value默认为一个固定的Object。

是通过hashCode值来确定集合中的位置,由于Set集合中并没有下标的概念,所以并没有像List一样提供一个get()方法,就可以获取元素。

当获取HashSet中某个元素时,只能通过遍历集合的方式进行equals()比较来实现。

核心方法(常用API)

 

public static void main(String[] args) {
    // 声明一个HashSet集合
    Set hashSet = new HashSet();

    // 往集合添加元素
    hashSet.add("1");
    hashSet.add("2");
    hashSet.add("2");
    hashSet.add("1");
    hashSet.add("3");
    System.out.println(hashSet); // [1, 2, 3]

    List list = new ArrayList();
    list.add("3");
    list.add("4");
    list.add("5");
    list.add("5");
    System.out.println(list); // [3, 4, 5, 5]
    // 往集合添加指定集合。会去除重复的元素
    hashSet.addAll(list);
    System.out.println(hashSet); // [1, 2, 3, 4, 5]

    // 移除集合里指定元素
    hashSet.remove("2");
    System.out.println(hashSet); // [1, 3, 4, 5]

    // 获取集合长度
    int size = hashSet.size();
    System.out.println(size); // 4

    // 判断集合是不是空的
    boolean empty = hashSet.isEmpty();
    System.out.println(empty); // false

    // 判断集合是否包含指定元素
    boolean contains = hashSet.contains("3");
    System.out.println(contains); // true

    List list1 = new ArrayList();
    list1.add("1");
    // 判断集合里是否包含了一个指定的集合
    boolean containsAll = hashSet.containsAll(list1);
    System.out.println(containsAll); // true
    list1.add("2");
    boolean containsAll2 = hashSet.containsAll(list1);
    System.out.println(containsAll2); // false

    System.out.println(hashSet); // [1, 3, 4, 5]
    System.out.println(list1); // [1, 2]
    // 保留指定集合里的内容,结果取交集
    hashSet.retainAll(list1);
    System.out.println(hashSet); // [1]

    // 清空集合
    hashSet.clear(); // []
}

LinkedHashSet

 

public class LinkedHashSet<E> extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {}

LinkedHashSet继承了HashSet,实现了Set、Cloneable和Serializable接口。

LinkedHashSet使用的是LinkedHashMap。

LinkedHashSet底层数据结构采用双向链表,可以保证元素的插入顺序,又因为是HashSet的子类,所以插入的元素又不能重复。

LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的元素时将有很好的性能,因为它以链表形式来实现的。

LinkedHashSet源码里只有一些构造函数,一些具体操作实现的方法都是用父类HashSet的。

 

TreeSet

 

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable{}

TreeSet继承了AbstractSet,实现了NavigableSet、Cloneable和Serializable接口。

我们发现TreeSet并没有实现Set接口,而是实现了NavigableSet,NavigableSet继承了SortedSet,SortedSet又继承了Set。

TreeSet是一个有序的集合,但是最终实现与Set还是需要遵守Set的特性,元素不会重复。

TreeSet是基于TreeMap实现的,底层数据结构是红黑树(二叉树)

TreeSet也是SortedSet接口的唯一实现类,通过接口名称,我们能推知出,TreeSet是可以排序的。

特点:

  • 支持排序(确保集合里的元素处于排序状态,不是插入的顺序排列的)
  • 元素不可重复
  • 树形结构
  • 不支持随机遍历,只能通过迭代器进行遍历

用来排序的, 可以指定一个顺序, 对象存入之后会按照指定的顺序排列

排序方法

  • 自然排序
  • 比较器排序(Comparator)

核心方法(常用API)

 

public static void main(String[] args) {

    // 声明Set集合
    TreeSet treeSet = new TreeSet();

    // 往集合添加元素
    treeSet.add("1");
    treeSet.add("2");
    treeSet.add("3");
    treeSet.add("4");
    treeSet.add("5");
    System.out.println(treeSet); // [1, 2, 3, 4, 5]

    // 获取集合长度
    int size = treeSet.size();
    System.out.println(size); // 5

    // 判断集合是不是为空
    boolean empty = treeSet.isEmpty();
    System.out.println(empty); // false

    // 判断集合是否包含某个元素
    boolean contains = treeSet.contains("2");
    System.out.println(contains);// true

    // 获取集合第一个元素
    Object first = treeSet.first();
    System.out.println(first); // 1

    // 获取集合最后一个元素
    Object last = treeSet.last();
    System.out.println(last); // 5

    // 返回小于给定键的最大键
    Object lower = treeSet.lower("3");
    System.out.println(lower); // 2

    // 返回小于或等于给定键的最大键
    Object floor = treeSet.floor("3");
    System.out.println(floor); // 3

    // 返回大于或等于给定键的最小键
    Object ceiling = treeSet.ceiling("3");
    System.out.println(ceiling); // 3

    // 移除最小元素并返回
    Object pollFirst = treeSet.pollFirst();
    System.out.println(pollFirst); // 1
    System.out.println(treeSet); // [2, 3, 4, 5]

    // 移除最大元素并返回
    Object pollLast = treeSet.pollLast();
    System.out.println(pollLast); // 5
    System.out.println(treeSet); // [2, 3, 4]

    // 移除集合内指定元素
    treeSet.remove("3");
    System.out.println(treeSet); // [1, 2, 4]

    // 清空集合元素
    treeSet.clear();
    System.out.println(treeSet); // []
}

TreeSet有一个比较坑的地方,看一下

在我们之间讲过到的集合里,对一个集合指定类型,进行`存放数据,是没有问题的,但是在TreeSet这是不行的。

 


// User对象
@Data
public class User implements Comparable<User>{
    private String name;
    private Integer age;

    public User() {
    }

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

public static void main(String[] args) {
       // 声明Set集合
    TreeSet<User> treeSetUser = new TreeSet();
       treeSetUser.add(new User("zhangsan", 22));
       treeSetUser.add(new User("lisi", 20));
       treeSetUser.add(new User("wangwu", 25));
       System.out.println(treeSetUser);
}

当执行上面方法时会抛出如下异常

Exception in thread "main" java.lang.ClassCastException: com.xxx.crdms.controller.User cannot be cast to java.lang.Comparable
	at java.util.TreeMap.compare(TreeMap.java:1294)
	at java.util.TreeMap.put(TreeMap.java:538)
	at java.util.TreeSet.add(TreeSet.java:255)
	at com.xxx.crdms.controller.TestDemoController.main(TestDemoController.java:34)

 

通过异常信息我们得知是出现了类型转换异常。原因在于我们需要告诉TreeSet如何来进行比较元素,如果不指定,就会抛出这个异常。

这个TreeSet我们认为是用来存储的,为什么要排序呢?

在上面介绍的时候就说了TreeSet有一个特点就是可以指定排序存储。

这个异常出现原因就是我们没有指定排序方式。

那怎么解决呢?

需要在我们指定TreeSet集合泛型的对象类里实现Comparable接口,并重写接口中的compareTo方法

就像这样,实现Comparable,重写compareTo方法。

 

@Data
public class User implements Comparable<User>{
    private String name;
    private Integer age;

    public User() {
    }

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

    @Override
    public int compareTo(User o) {
        if(this.age > o.age){
            return 1;
        }else if(this.age < o.age){
            return -1;
        }else{
            return this.name.compareTo(o.name);
        }
    }
}
// 这里返回值有三中情况。对于这个方法里的排序方式根据需求来写。这只是举栗子
return 0;               //当compareTo方法返回0的时候集合中只有一个元素
return 1;               //当compareTo方法返回正数的时候集合会怎么存就怎么取
return -1;              //当compareTo方法返回负数的时候集合会倒序存储

如果将compareTo()返回值写死为0,元素值每次比较,都认为是相同的元素,这时就不再向TreeSet中插入除第一个外的新元素。所以TreeSet中就只存在插入的第一个元素。

如果将compareTo()返回值写死为1,元素值每次比较,都认为新插入的元素比上一个元素大,于是二叉树存储时,会存在根的右侧,读取时就是正序排列的。

如果将compareTo()返回值写死为-1,元素值每次比较,都认为新插入的元素比上一个元素小,于是二叉树存储时,会存在根的左侧,读取时就是倒序序排列的。

这种排序称为自然排序(Comparable)

TreeSet自然排序步骤

  1. 需要排序的类实现Comparable接口
  2. 重写compareTo方法,方法内自定义排序方式。

实现了自然排序之后在去执行那个main方法就不会出现异常了。

上边说了两种排序,另一个就是比较器的排序了。看下如何实现

第一种方式:通过一个匿名的内部类来实现

public static void main(String[] args) {

    TreeSet<User> treeSetUser = new TreeSet<User>(new Comparator<User>() {
        @Override
        public int compare(User u1, User u2) {
            // 先判断姓名长度的大小
       int num = u1.getName().length() - u2.getName().length();
            // 姓名长度一致时,比较内容是否一致
       int num2 = num==0 ?u1.getName().compareTo(u2.getName()) :num;
            // 姓名内容一致时,比较年龄
       int num3 = num2==0 ?(u1.getAge() - u2.getAge()) :num2;
            return num3;
        }
    });

    treeSetUser.add(new User("zhangsan", 22));
    treeSetUser.add(new User("lisi", 20));
    treeSetUser.add(new User("wangwu", 25));

    System.out.println(treeSetUser);
}
输出:
[User(name=lisi, age=20), User(name=wangwu, age=25), User(name=zhangsan, age=22)]

 

第二种方式:自定义一个类,实现Comparator,重写compare方法

public class ComparatorSort implements Comparator<User> {
    @Override
    public int compare(User u1, User u2) {
        // 先判断姓名长度的大小
        int num = u1.getName().length() - u2.getName().length();
        // 姓名长度一致时,比较内容是否一致
        int num2 = num==0 ?u1.getName().compareTo(u2.getName()) :num;
        // 姓名内容一致时,比较年龄
        int num3 = num2==0 ?(u1.getAge() - u2.getAge()) :num2;
        return num3;
    }
}

public static void main(String[] args) {
    TreeSet<User> treeSetUser = new TreeSet<>(new ComparatorSort());
    treeSetUser.add(new User("zhangsan", 22));
    treeSetUser.add(new User("lisi", 20));
    treeSetUser.add(new User("wangwu", 25));
    System.out.println(treeSetUser);
}
输出:
[User(name=lisi, age=20), User(name=wangwu, age=25), User(name=zhangsan, age=22)]

同样两种方式都可实现比较器排序。

 

总结

Set是一个接口,继承Collection接口。

Set是一个无序,元素不可重复的集合。

HashSet是Set接口的一个实现类,依赖于hashCode()与equals()方法。

在存储时首先首先比较哈希值:

  1. 如果相同比较地址值或者equals(),相同说明元素重复不添加,不同说明元素不重复,添加到集合中
  2. 不相同直接添加到集合中

如果一个类中没有重写hashCode()和equals()则直接继承Object类

LinkedHashSet是HashSet的一个子类,也是Set集合比较特殊的集合,是一个有序的set集合。底层是通过链表与哈希表来实现的。

TreeSet也是Set接口的一个实现类,是一个排序集合。能够对元素按照某种规则进行排序存储。

排序方式有两种:自然排序和比较器排序。

自然排序是将存储的对象实现Comparable接口,重写compareTo()方法,自定义排序方式。

比较器排序可以指定内部匿名类或新建类实现Comparator接口,都需要重写compare方法,来自定义排序方式。

HashSet底层是通过HashMap实现的,查询比较快,元素必须定义hashCode方法。(比较常用)

LinkedHashSet底层数据结构是链表,具有HashSet的查询速度; 内部使用链表维护元素插入的次序; 元素必须定义hashCode方法。

TreeSet底层数据结构是二叉树结构,可以从Set中提取有序的序列; 元素必须实现Comparable接口。

 


 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值