今天的博客主题
基础篇 --》常用类 --》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自然排序步骤
- 需要排序的类实现Comparable接口
- 重写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()方法。
在存储时首先首先比较哈希值:
- 如果相同比较地址值或者equals(),相同说明元素重复不添加,不同说明元素不重复,添加到集合中
- 不相同直接添加到集合中
如果一个类中没有重写hashCode()和equals()则直接继承Object类
LinkedHashSet是HashSet的一个子类,也是Set集合比较特殊的集合,是一个有序的set集合。底层是通过链表与哈希表来实现的。
TreeSet也是Set接口的一个实现类,是一个排序集合。能够对元素按照某种规则进行排序存储。
排序方式有两种:自然排序和比较器排序。
自然排序是将存储的对象实现Comparable接口,重写compareTo()方法,自定义排序方式。
比较器排序可以指定内部匿名类或新建类实现Comparator接口,都需要重写compare方法,来自定义排序方式。
HashSet底层是通过HashMap实现的,查询比较快,元素必须定义hashCode方法。(比较常用)
LinkedHashSet底层数据结构是链表,具有HashSet的查询速度; 内部使用链表维护元素插入的次序; 元素必须定义hashCode方法。
TreeSet底层数据结构是二叉树结构,可以从Set中提取有序的序列; 元素必须实现Comparable接口。