深入理解Java Set集合特性
1. Set概述
java.util.Set 接口继承了 Collection 接口,是常用的一种集合类型。 相对于之前学习的List集合,Set集合特点如下:
除了具有 Collection 集合的特点,还具有自己的一些特点:
Set是一种无序的集合
Set是一种不带下标索引的集合
Set是一种不能存放重复数据的集合
Set接口继承体系:
Set接口方法:
Set集合实现类:
重点学习的Set实现类:
HashSet 底层借助哈希表实现
TreeSet 底层借助二叉树实现
注意,TreeSet是Set接口的子接口SortedSet的实现类
基础案例:
实例化一个Set集合,往里面添加元素并输出,注意观察集合特点(无序、不 重复)。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;public class Test071_SetBasic {public static void main(String[] args) {// 1. 实例化Set集合,指向HashSet实现类对象Set<String> set = new HashSet<>();set.add("hello1");set.add("hello2");set.add("hello3");set.add("hello4");set.add("hello5"); // 重复元素,添加失败set.add("hello5"); // 重复元素,添加失败// 使用增强for循环遍历for (String obj : set) {System.out.println(obj);}System.out.println("-----------------");// 使用迭代器遍历Iterator<String> it = set.iterator();while (it.hasNext()) {String obj = it.next(); // 建议使用String而非Object,保持类型一致System.out.println(obj);}}
}hello1
hello4
hello5
hello2
hello3
-----------------
hello1
hello4
hello5
hello2
hello3
通过以上代码和运行结果,可以看出Set类型集合的特点:无序、不可重复
2. HashSet
java.util.HashSet 是Set接口的实现类,它使用哈希表(Hash Table)作为其 底层数据结构来存储数据。
HashSet特点:
无序性:HashSet中的元素的存储顺序与插入顺序无关 HashSet使用哈希表来存储数据,哈希表根据元素的哈希值来确定元素的存 储位置,而哈希值是根据元素的内容计算得到的,与插入顺序无关。
唯一性:HashSet中不允许重复的元素,即每个元素都是唯一的
允许null元素:HashSet允许存储null元素,但只能存储一个null元素, HashSet中不允许重复元素
高效性:HashSet的插入、删除和查找操作的时间复杂度都是O(1) 哈希表通过将元素的哈希值映射到数组的索引来实现快速的插入、删除和查 找操作。
案例:
实例化HashSet对象,往里面存入多个自定义类Student对象,观察结果。
自定义Student类:
public class Student {private String name;private int age;public Student() {}public Student(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;}@Overridepublic String toString() {return "Student [name=" + name + ", age=" + age + "]";}
}
测试类:
import java.util.HashSet;
import java.util.Set;
import com.briup.chap08.bean.Student;public class Test072_Student {public static void main(String[] args) {// 1. 实例化HashSet对象Set<Student> set = new HashSet<>();// 2. 往集合中添加元素set.add(new Student("zs", 20));set.add(new Student("zs", 20));set.add(new Student("zs", 20));set.add(new Student("zs", 20));System.out.println("size: " + set.size());// 3. 遍历for (Student stu : set) {System.out.println(stu);}}
}
从结果可知,HashSet认为4个数据成员一模一样的Student对象是不同的对象, 成功将它们添加到了集合中。这明显是不合理的,思考为什么?
元素插入过程:
- 当向HashSet中插入元素时,先获取元素的hashCode()值,再检查HashSet中 是否存在相同哈希值的元素,如果元素哈希值唯一,则直接插入元素
- 如果存在相同哈希值的元素,会调用元素的equals()方法来进一步判断元素 是否是相同。如果相同,则不会插入重复元素;如果不同,则插入
hashCode() 方法复习:
- hashCode() 方法是 Object类 中的一个方法,它返回一个 码值
- 通常情况下, int 类型的哈希 hashCode() 方法会根据对象的属性值计算一个哈希码值(重 写自定义类中 hashCode方法 )
- 如果两个对象的hashCode值不同,则两个对象的属性值肯定不同
- 如果两个对象的hashCode值相同,则两个对象的属性值可能相同,也可能不 同
在案例的基础上,重写Student类的hashCode和equals方法,再次运行程序观 察结果。
public class Student {private String name;private int age;// 省略,跟之前的一样...// 重写 hashCode 和 equals 方法,Alt+Shift+S 自动生成即可// 自动生成的 hashCode,根据类属性值计算得到哈希码@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + age;result = prime * result + ((name == null) ? 0 : name.hashCode());return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;Student other = (Student) obj;if (age != other.age)return false;if (name == null) {if (other.name != null)return false;} else if (!name.equals(other.name))return false;return true;}
}
重新运行程序,结果如下:
结论:如果要往HashSet集合存储自定义类对象,那么一定要重写自定义类中的 hashCode方法和equals方法。
3. TreeSet
TreeSet是SortedSet(Set接口的子接口)的实现类,它基于红黑树(Red Black Tree)数据结构实现。
TreeSet特点:
有序性:插入的元素会自动排序,每次插入、删除或查询操作后,TreeSet会 自动调整元素的顺序,以保持有序性。
唯一性:TreeSet中不允许重复的元素,即集合中的元素是唯一的。如果尝试 插入一个已经存在的元素,该操作将被忽略。
动态性:TreeSet是一个动态集合,可以根据需要随时添加、删除和修改元 素。插入和删除操作的时间复杂度为O(log n),其中n是集合中的元素数量。
高效性:由于TreeSet底层采用红黑树数据结构,它具有快速的查找和插入性 能。对于有序集合的操作,TreeSet通常比HashSet更高效。
案例:
准备一个TreeSet集合,放入多个自定义类Person对象,观察是否自动进行排序。
import java.util.Set;
import java.util.TreeSet;class Person {private String name;private int age;public Person() {}public Person(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;}@Overridepublic String toString() {return "Person [name=" + name + ", age=" + age + "]";}
}public class Test073_Person {public static void main(String[] args) {// 1. 实例化 TreeSetSet<Person> set = new TreeSet<>();// 2. 添加元素set.add(new Person("zs", 21));set.add(new Person("ls", 20));set.add(new Person("tom", 19));// 3. 遍历集合for (Person person : set) {System.out.println(person);}}
}//程序运行,提示异常,具体如下:
Exception in thread "main" java.lang.ClassCastException:
com.briup.chap08.test.Person cannot be cast to
java.lang.Comparableat java.util.TreeMap.compare(TreeMap.java:1290)at java.util.TreeMap.put(TreeMap.java:538)at java.util.TreeSet.add(TreeSet.java:255)at
com.briup.chap08.test.Test073_Person.main(Test073_Person.java:41)
问题分析:
根据异常提示可知错误原因是: Person 无法转化成 ClassCastException异常 。 为什么会这样呢?
TreeSet是一个有序集合,存储数据时,一定要指定元素的排序规则,有两种指 定的方式,具体如下:
TreeSet排序规则:
自然排序(元素所属类型要实现 java.lang.Comparable 接口)
比较器排序
如果将Integer存入TreeSet不会报错,是因为Integer类已经实现自然排 序,而Person类既没有实现自然排序,也没有额外指定比较器排序规则。
1)TreeSet:自然排序
如果一个类,实现了 java.lang.Comparable 接口,并重写了 compareTo方法,那么这个类的对象就是可以比较大小的。
public interface Comparable<T> {public int compareTo(T o);}
compareTo方法说明:
int result = this.属性.compareTo(o.属性);
- result的值大于0,说明this比o大
- result的值小于0,说明this比o小
- result的值等于0,说明this和o相等
元素插入过程:
当向TreeSet插入元素时,TreeSet会使用元素的 compareTo() 方法来比较元素 之间的大小关系。根据比较结果,TreeSet会将元素插入到合适的位置,以保持 有序性。如果两个元素相等( compareTo() 方法返回0),TreeSet会认为这是 重复的元素,只会保留一个。
案例:
使用自然排序(先按name升序,name相同则按age降序)解决上述案例问 题。
基础Person类:
// 定义Person类,实现自然排序
public class Person implements Comparable<Person> {private String name;private int age;public Person() {}public Person(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;}@Overridepublic String toString() {return "Person [name=" + name + ", age=" + age + "]";}// 重写方法,指定比较规则:先按name升序,name相同则按age降序@Overridepublic int compareTo(Person o) {// 注意:字符串比较需要使用compareTo方法int r = name.compareTo(o.name);if (r == 0) {r = o.age - this.age; // 年龄降序}return r;}
}
测试类:
import java.util.Set;
import java.util.TreeSet;
import com.briup.chap08.bean.Person;// 自然排序测试
public class Test073_Comparable {public static void main(String[] args) {// 1. 实例化 TreeSetSet<Person> set = new TreeSet<>();// 2. 添加元素set.add(new Person("zs", 21));set.add(new Person("ww", 20));set.add(new Person("zs", 21));set.add(new Person("tom", 19));set.add(new Person("tom", 23));set.add(new Person("jack", 20));// 3. 遍历集合for (Person person : set) {System.out.println(person);}}
}//输出结果:
Person [name=jack, age=20]Person [name=tom, age=23]Person [name=tom, age=19]Person [name=ww, age=20]Person [name=zs, age=21]
补充内容:整形、浮点型、字符串自然排序规则
对于整型、浮点型元素,它们会按照从小到大的顺序进行排序。对于字符串类 型的元素,它们会按照字典顺序进行排序。
注意事项:compareTo方法的返回结果,只关心正数、负数、零,不关心具体的值是多少
2)TreeSet:比较器排序
思考:如果上述案例中Person类不是自定义类,而是第三方提供好的(不可以修 改源码),那么如何实现排序规则的指定?
我们可以可以使用比较器(Comparator)来自定义排序规则。
比较器排序步骤:
- 创建一个实现了Comparator接口的比较器类,并重写其中的 compare() 方法 ,该方法定义了元素之间的比较规则。在 compare() 方法中,我们可以根据 元素的属性进行比较,并返回负整数、零或正整数,来表示元素之间的大小 关系。
- 创建TreeSet对象时,将比较器对象作为参数传递给构造函数,这样,TreeSet 会根据比较器来进行排序。
java.util.Comparator 接口源码:
package java.util;@FunctionalInterface
public interface Comparator<T> {/*** Compares its two arguments for order. Returns a negative integer,* zero, or a positive integer as the first argument is less than, equal* to, or greater than the second.*/int compare(T o1, T o2);// 省略...
}
compare方法说明:
int result = compare(o1, o2);
result的值大于0,表示升序
result的值小于0,表示降序
result的值等于0,表示元素相等,不能插入
注意,这里和自然排序的规则是一样的,只关心正数、负数、零,不关心具体的 值是多少
案例:
使用比较器排序,对上述案例中Person进行排序,要求先按age升序,age相同 则按name降序。
基础类需要注释掉自然排序接口:
// 定义Person类,实现自然排序
public class Person /* implements Comparable<Person> */ {private String name;private int age;public Person() {}public Person(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;}@Overridepublic String toString() {return "Person [name=" + name + ", age=" + age + "]";}// 重写方法,指定比较规则// @Override// public int compareTo(Person o) {// // 先按name升序,name相同则按age降序// int r = name.compareTo(o.name);// if (r == 0) {// r = o.age - this.age;// }// return r;// }
}
测试类:
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
import com.briup.chap08.bean.Person;// 比较器排序测试
public class Test073_Comparator {public static void main(String[] args) {// 1. 准备自定义比较器对象(匿名内部类形式)Comparator<Person> comp = new Comparator<Person>() {// 重写比较算法:先按 age 升序,age 相同则按 name 降序@Overridepublic int compare(Person o1, Person o2) {int r = o1.getAge() - o2.getAge();if (r == 0) {// 注意:字符串比较需要使用 compareTo 方法r = o2.getName().compareTo(o1.getName()); // name 降序}return r;}};// 2. 实例化 TreeSet,传入自定义比较器对象Set<Person> set = new TreeSet<>(comp);// 3. 添加元素set.add(new Person("zs", 21));set.add(new Person("ww", 20));set.add(new Person("zs", 21));set.add(new Person("tom", 19));set.add(new Person("tom", 23));set.add(new Person("jack", 20));// 4. 遍历集合for (Person person : set) {System.out.println(person);}}
}//输出结果:
Person [name=tom, age=19]Person [name=ww, age=20]Person [name=jack, age=20]Person [name=zs, age=21]Person [name=tom, age=23]
注意:如果同时使用自然排序和比较器排序,比较器排序将覆盖自然排序。
4. LinkedHashSet
LinkedHashSet 是 HashSet 的一个子类,具有 HashSet 的高效性能和唯一性 特性,并且保持了元素的插入顺序,其底层基于哈希表和链表实现。
案例:
实例化一个LinkedHashSet集合对象,存入多个String字符串,观察是否唯一及 顺序存储。
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;public class Test074_LinkedHashSet {public static void main(String[] args) {// 1. 实例化 LinkedHashSetSet<String> set = new LinkedHashSet<>();// 2. 添加元素set.add("bbb");set.add("aaa");set.add("abc");set.add("bbc");set.add("abc");// 3. 迭代器遍历Iterator<String> it = set.iterator();while (it.hasNext()) {System.out.println(it.next());}}
}//运行结果:
bbbaaaabcbbc