Set
Set集合分类
根据不同集合的底层数据结构以及其特点,Set集合分为三类
- HashSet
- 底层数据结构为哈希表,存取无序且元素唯一
- LinkedHashSet
- 底层数据结构为链表和哈希表,存取有序且元素唯一
- TreeSet
- 元素唯一 且可以通过自然排序或比较器排序,对元素进行排序。
常用方法
Set属于Collection的子接口,所以Set集合可以直接调用Collection的方法,现在介绍Set子接口特有的方法:
Set常用方法 | 功能 |
---|---|
boolean add(E e) | 如果 set 中尚未存在指定的元素,则添加此元素 |
void clear() | 移除此 set 中的所有元素 |
boolean contains( ) | 如果 set 包含指定的元素,则返回 true |
boolean isEmpty( ) | 判断Set集合是否为空集合 |
Iterator<E> irerator( ) | 返回一个在此Set集合元素上进行迭代的迭代器 |
boolean remove(Object o) | 如果Set集合中存在指定元素,则将其移除 |
int size( ) | 返回Set集合中的元素数 |
Object[] toArray( ) | 返回一个包含Set集合中所有元素的数组 |
HashSet
特点
- HashSet集合底层数据结构为哈希表,存取无序且元素唯一
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值决定该对象在 HashSet 中的存储位置。
原理
- HashSet 集合判断两个元素相等的标准:
两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
当我们定义一个HashSet集合,向集合中添加集合Integer类对象时,当其中有重复元素,之后添加的会将原有的覆盖:
public class Test {
public static void main(String[] args) {
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(new Integer(20));
hashSet.add(new Integer(20));
hashSet.add(new Integer(10));
hashSet.add(new Integer(10));
System.out.println(new Integer(20).equals(new Integer(20)));
System.out.println(new Integer(10).equals(new Integer(10)));
System.out.println(hashSet);
}
}
因为两个Integer对象通过equal方法返回是true,所以HashSet集合只会存储一个对象。
现在我们用HashSet集合存储几个学生类的对象:
- 学生类:
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Studeng{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- Set集合:
public class Test {
public static void main(String[] args) {
HashSet<Student> hashSet = new HashSet<>();
Student stu1 = new Student("小明",20);
Student stu2 = new Student("小红",18);
Student stu3 = new Student("小明",20);
Student stu4 = new Student("小红",18);
hashSet.add(stu1);
hashSet.add(stu2);
hashSet.add(stu3);
hashSet.add(stu4);
System.out.println(stu1.equals(stu3));
System.out.println(stu2.equals(stu4));
System.out.println(hashSet);
}
}
会发现两个成员变量完全相同的学生类对象存储到HashSet集合中,并没有元素唯一,这是因为equal()方法默认比较的是两个对象的地址值,并没有比较对象的成员变量,所以当我们想达在HashSet集合中通过比较成员变量实现元素唯一时,我们需要重写hashCode()和equals()方法,重写后HashSet就能根据成员变量值存储唯一元素。
- 重写equal()和hashCode()方法 , 可通过快捷键Alt+Insert:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
这时HashSet集合就会根据成员变量值存储唯一元素,运行结果:
LinkedHashSet
特点
- 底层数据结构有两个 链表和哈希表
链表保证有序 、哈希表保证元素唯一
LinkedHashSet集合和HashSet集合同样可以保证元素唯一性,LinkedHashSet集合还能通过链表保证元素的输入输出有序。现在用HashSet集合和LinkHashSet集合按照相同顺序存储相同的字符元素:
- HashSet集合
public class Test {
public static void main(String[] args) {
HashSet<Character> hashSet = new HashSet<>();
hashSet.add('e');
hashSet.add('d');
hashSet.add('a');
hashSet.add('c');
hashSet.add('b');
System.out.println(hashSet);
}
}
会发现数组中存储的顺序和我们添加元素的顺序并不相同,是因为HashSet集合是根据哈希表对元素进行排序。
- LinkedHashSet集合
public class Test {
public static void main(String[] args) {
LinkedHashSet<Character> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add('e');
linkedHashSet.add('d');
linkedHashSet.add('a');
linkedHashSet.add('c');
linkedHashSet.add('b');
System.out.println(linkedHashSet);
}
}
LinkedHashSet集合对元素的存储会通过链表的形式,在元素唯一的基础上,使元素在集合中存储的顺序和添加顺序一致。
TreeSet
特点
- 元素唯一,并且可以对元素进行排序
原理
TreeSet中存储元素,不仅可以达到元素的唯一性,还能对元素进行排序。
这是因为TreeSet集合的底层数据结构为二叉树(红黑树)。
当存储一个新的元素时,会和根节点的元素值进行比较。如果大于,则会存储到根节点的右边,小于则会存到坐标左边,值相等则不储存。这样就达到了元素唯一且排序的目的。
自然排序
现在在TreeSet中存储一些Integer类型的数据:
ublic class Test {
public static void main(String[] args) {
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(20);
treeSet.add(10);
treeSet.add(50);
treeSet.add(20);
treeSet.add(10);
treeSet.add(30);
System.out.println(treeSet);
}
}
这时TreeSet集合自动覆盖掉了重复元素并且将元素排序。
那我们现在存储一些学生类的对象:
public class Test {
public static void main(String[] args) {
TreeSet<Student> treeSet = new TreeSet<>();
treeSet.add(new Student("小明",20));
treeSet.add(new Student("小红",18));
treeSet.add(new Student("小明",23));
treeSet.add(new Student("小红",18));
treeSet.add(new Student("李雷",25));
System.out.println(treeSet);
}
}
这时我们运行会发现系统报错:ClassCastException(Java强制类型转换异常)。
原因是当我们在TreeSet存储数据时,TreeSet想要进行自然排序,前提是我们存储的元素必须实现Comparable接口,否则无法进行自然排序。换句话说我们存储Integer类型,Comparable接口可以判断出元素值的大小,但是存储Student对象,接口无法判断。
如果我们想要可以将对象按照某个标准进行排序,那就需要让学生类来实现Comparable接口,要实现接口,需要重写接口的compareTo方法。
- 例如,我们需要TreeSet集合按照学生年龄进行排序:
- 让学生类实现Comparable接口:
public class Student implements Comparable<Student>{
}
- 重写compareTo方法:
此方法返回一个int值,若值大于0,则排后面;小于0,排前面;等于0,不储存。
当然如果年龄相同姓名不同需要根据compareTo方法默认对字符串的比较来排序。
@Override
public int compareTo(Student student) {
int num=this.age-student.age;
int num2=num==0?this.name.compareTo(student.name):num;
return num2;
}
- 这时在打印TreeSet的存储数据,就不会在出现系统报错。并且会根据重写的方法,按照年龄进行排序:
当然如果想倒排序,只需要重写compareTo方法时返回负值即可:
@Override
public int compareTo(Student student) {
int num=this.age-student.age;
int num2=num==0?this.name.compareTo(student.name):num;
return -num2;
}
比较器排序
当我们使用空参构造创建TreeSet集合时,会只用自然排序,需要实现Comparable接口。我们也可以采用有参构造TreeSet(Comparator < ? super E > comparator)
,传入一个Comparetor 比较器:
TreeSet<Student> treeSet = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return 0;
}
});
比较用来排序的两个参数,现在按照学生姓名长度进行排序(当长度相等时按默认排序方式,姓名相同按年龄排序):
public class Test {
public static void main(String[] args) {
TreeSet<Student> treeSet = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int num = o1.getName().length() - o2.getName().length();
int num2=num==0?o1.getName().compareTo(o2.getName()):num;
int num3=num2==0?o1.getAge()-o2.getAge():num2;
return num3;
}
});
treeSet.add(new Student("易烊千玺",20));
treeSet.add(new Student("王源",18));
treeSet.add(new Student("王俊凯",23));
treeSet.add(new Student("Jay Chou",18));
treeSet.add(new Student("韩梅梅",23));
treeSet.add(new Student("王俊凯",15));
for (Student student : treeSet) {
System.out.println(student);
}
}
}
总结
当我们想存储无重复元素时,通常使用Set集合。Set集合可以保证元素的唯一性。
HashSet效率最高,LinkedHashSet输入输出有序,TreeSet同时满足元素唯一和排序。