一、set概述
1.1概念
Set是Java中的一个集合类型,它继承自Collection接口,表示一组不包含重复元素的无序集合,本质上是一个容器。
1.2体系
Set集合由HashSet、TreeSet、LinkedHashSet组成。
1.3特点
从set集合的概念中我们就能看出其两个特点:
1.3.1 无序性
Set集合中的元素没有顺序之分,即元素无法通过索引访问
1.3.2 唯一性
Set集合中的元素不可重复,即无法有两个相同的元素存在于同一个Set集合中
1.4 通用方法
方法 | 说明 |
add(E e) |
将指定元素添加到集合中(如果已经存在则不添加,返回false) |
remove(Object o) | 从集合中移除指定元素(如果元素不存在则不进行操作,返回false) |
contains(Object o) | 判断集合中是否包含指定元素,即判断指定元素是否是集合中的一个元素 |
isEmpty() | 判断集合是否为空 |
size() |
返回集合中元素的个数 |
clear() | 从集合中移除所有元素 |
二、遍历方式
对比List集合的三种遍历方式,我们再根据Set集合的特点——无序性,可以得出Set集合只有两种遍历方式,缺少for循环根据下标遍历
2.1foreach
public static void main(String[] args) { Set s = new TreeSet(); s.add(1); s.add(2); s.add(3); for (Object object : s) { System.out.println(object);// 1 2 3 } }
2.2迭代器
public static void main(String[] args) { Set s = new TreeSet(); s.add(1); s.add(2); s.add(3); Iterator iterator = s.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next());// 1 2 3 } }
三、HashSet
3.1 HashSet概述
3.1.1概念
HashSet是Java中的一个类,实现了Set接口,底层使用哈希表(来存储元素,是Java集合框架中常用的一种无序、不可重复的集合类型。
3.1.2数据结构
HashSet的数据结构是哈希表
哈希表(
hash table
,也称散列表)是一种根据关键码值(
key value)
进行直接访问的数据结构。哈希表通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。换句话说,哈希表中的每一个元素都是通过哈希算法计算出来的一个索引,通过该索引可以快速在表中查找到相应的元素哈希表的实现方法主要包括开放寻址法和链表法两种。在开放寻址法中,哈希表会将发生冲突的元素放入不同的槽位里,因此需要对哈希表进行线性、二次探测等操作,期望时间复杂度为O(1),均摊时间复杂度为O(1)。而链表法则是在哈希表上使用单向链表或者双向链表来解决哈希碰撞的问题,产生碰撞的元素会被放入同一个链表中,以保证时间复杂度尽量地接近理想状态。
哈希表底层实现常用的数据结构是数组。虽然哈希表底层是通常数组,但其动态扩容、迁移元素等操作的实现,使得哈希表具有比数组更加灵活和高效的特点。
3.2HashSet去重原理
3.2.1字符串去重
HashSet s = new HashSet(); s.add("a"); s.add("b"); s.add("c"); System.out.println(s);//[a, b, c] s.add("b"); System.out.println(s);//[a, b, c]
因为HashSet的特点之一就是唯一性,所以“b”没有新增成功,那么原理是什么呢?
当我们将多个字符串添加到HashSet集合中时,HashSet会根据字符串的哈希值来进行判断,如果哈希值相同并且两个字符串相等(使用String类的equals()方法进行比较,即比较内存地址),则认为这是同一个字符串,只会保留一个。
3.2.2对象去重
public static void main(String[] args) { HashSet s = new HashSet(); s.add(new Student("zs", 16)); s.add(new Student("ls", 17)); s.add(new Student("ww", 18)); System.out.println(s);//[Student{name='zs', age=16}, Student{name='ww', age=18}, Student{name='ls', age=17}] s.add(new Student("ww", 18)); System.out.println(s);//[Student{name='zs', age=16}, Student{name='ww', age=18}, Student{name='ww', age=18}, Student{name='ls', age=17}] }
那为什么new Student("ww", 18)新增成功呢?
是的,与上篇博客ArrayList集合对象去重类似,当学习的事物有共性时,你要有举一反三的能力,这样你会感到一种通透,就像打通任督二脉一般,没错,这是因为实例化了,使得第二个 Student("ww", 18)新开辟了一个内存地址,这让两个对象的hash值也不同,导致新增成功!
怎么解决呢,达到去重的效果?
没错,看过上篇博客的你又能把答案脱口而出,重写equals方法!什么是学习的快乐?这就是!!拥有了举一反三的能力是在学习时是无比快乐的,话不多说,让我们快点开始操作吧,上代码!
package com.xqx.set; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.TreeSet; public class demo1 { public static void main(String[] args) { HashSet s = new HashSet(); s.add(new Student("zs", 16)); s.add(new Student("ls", 17)); s.add(new Student("ww", 18)); System.out.println(s);//[Student{name='zs', age=16}, Student{name='ww', age=18}, Student{name='ls', age=17}] s.add(new Student("ww", 18)); System.out.println(s);//[Student{name='zs', age=16}, Student{name='ww', age=18}, Student{name='ww', age=18}, Student{name='ls', age=17}] } } class Student { private String name; private int age; public Student(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; } // @Override public boolean equals(Object o) { System.out.println("调用了我心爱的equals方法"); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && name.equals(student.name); } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
可是,为什么重写equals方法后,它还是没有去重,甚至都没打印“调用了我心爱的equals方法”?没事,不要伤心不要难过,这就像同一种数学题型,解题总的思路是一致的,但在某一步骤可能需要变通一下。让我们来回顾字符串去重的原理,“当我们将多个字符串添加到HashSet集合中时,HashSet会根据字符串的哈希值来进行判断”,看到哈希值是否给你提供了解题灵感?
没错!聪明的你已经想出来了,就是重写HashCode方法!!!
package com.xqx.set; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.TreeSet; public class demo1 { public static void main(String[] args) { HashSet s = new HashSet(); s.add(new Student("zs", 16)); s.add(new Student("ls", 17)); s.add(new Student("ww", 18)); System.out.println(s);//[Student{name='ww', age=18}, Student{name='zs', age=16}, Student{name='ls', age=17}] s.add(new Student("ww", 18)); System.out.println(s);//[Student{name='ww', age=18}, Student{name='zs', age=16}, Student{name='ls', age=17}] } } class Student { private String name; private int age; public Student(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; } // @Override public boolean equals(Object o) { System.out.println("调用我心爱equals方法。。。"); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && name.equals(student.name); } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int hashCode() { System.out.println("调用我最爱的hashCode方法"); return Objects.hash(name, age); } }
打印结果:
OK,到这里已经实现了对象去重,但我知道你小小的脑袋现在是充满了大大的疑惑的。
没事,哥会为你解答
问题1:为什么重写equals方法后,它没有去重,甚至都没打印“调用了我心爱的equals方法”?
答:虽然重写了equals
方法,但是没有重写hashCode
方法。在判断元素是否相同时,HashSet
首先会根据被添加元素的哈希值来确定它应该插入集合的位置。因为Student
类没有重写hashCode
方法,所以HashSet
会默认使用Object
类的hashCode
方法,根据对象的内存地址计算哈希值。在new Student("ww", 18)
被添加到集合中时,它的哈希值与集合中已有元素的哈希值不同,因此会被添加到集合中,而哈希值不同,根本就不会调用equals
方法,所以不会打印。
问题2:为什么重写hashCode
方法后,每新增一次就会调用一次该方法?
答:由于每个Student
对象的哈希值都是根据其属性值计算得到的,并且对象属性值不同,所以每次新增一个新的Student
对象都会调用一次更新后的hashCode
方法,根据对象的属性值生成哈希值,如果已经有相同哈希值的元素了,则使用equals
方法判断是否真的相等,如果是则不进行插入,否则仍然会插入,这就是为什么每新增一次就会调用一次的原因。
3.2.3总结
无论是字符串去重,还是对象去重,HashSet都会比较Hash值,再去比较内存地址,只有这两者都相同才会去重。
- 如果是字符串去重,
HashSet会
默认实现 - 如果是对象去重,需要确保对象重写了
hashCode()
方法和equals()
方法
四、TreeSet
4.1概述
4.1概念
TreeSet
是一个基于红黑树(Red-Black Tree)实现的有序集合类,实现了set接口,在集合中的元素按照一定的顺序排列。
4.2数据结构
TreeSet
的数据结构是红黑树(以下内容不太了解)
红黑树是一种自平衡二叉搜索树,它能够保证在最坏情况下基本动态集合操作的时间复杂度为 O(log n),非常适合用于数据访问较为频繁、插入和删除操作不是特别频繁的场景。
红黑树在结构上是由满足以下五个条件的二叉搜索树组成的:
- 树中的每个节点都是黑色或红色。
- 根节点是黑色的。
- 如果一个节点为红色,则它的子节点必须是黑色的。
- 每个叶子节点(NIL节点,不包含键值的节点)都是黑色的。
- 从任意一个节点到叶子节点的每个路径上,所包含的黑色节点数目相同。
红黑树是一种自平衡的二叉搜索树,这意味着插入或删除节点时,红黑树会通过旋转和变色操作来保持满足上面的五个条件。通过自平衡,红黑树能够很快地返回排序结果或完成查找、插入、删除等操作。
红黑树的优点包括:
- 能够在 O(log n) 的时间复杂度内完成基本的动态集合操作,包括插入、删除、查找等;
- 在树形结构上作了特殊限制,避免了二叉搜索树可能退化成线性链的情况;
- 具有平衡性质,自平衡的速度较快,使得相应的算法更具优势。
平衡二叉搜索树(Balanced Binary Search Tree,简称 BBST)是一种基于二叉树的数据结构,在二叉搜索树的基础上,通过一些自平衡策略,使得每个节点的左右子树高度相差不超过1,从而保证在最坏情况下基本动态集合操作的时间复杂度为 O(log n)。
常见的 BBST 包括 AVL 树、红黑树、B-树等。其中红黑树是一种常用的 BBST,能够在插入、删除元素时自动地进行节点的旋转和颜色的变换,以保证树的平衡。其他的 BBST,例如 AVL 树和 Treap 等,也具有类似的自平衡机制。
BBST 的特点是:
- 数据按照一定的顺序存储,支持快速的搜索、插入和删除操作;
- 树的高度对数据的总运行时间有重要影响;
- 通过高效的自平衡机制能够保证树的高度始终在 O(log n) 级别。
二叉树是一种常见的树形数据结构,它由一组节点和一组有向边组成,每个节点最多有两个子节点。特别的,二叉树中的每个节点都可以称为是它之下节点的父节点,而它之上的节点则是它的祖先节点。同时,一个节点的子节点被称为是它的后代节点。
二叉树的特点是:
- 每个节点最多有两个子节点,称为左子节点和右子节点;
- 左子节点的值小于父节点,右子节点的值大于父节点;
- 左右子树都是二叉树,它们也都满足二叉搜索树的性质;
- 每个节点只有一个父节点,除了根节点没有父节点以外。
4.2TreeSet的特点
- 必须实现
Comparable
接口或者传入一个比较器(Comparator
)。 - 内部基于红黑树实现,保证元素有序。
- 这里的“有序”与List集合的有序性不同,
List
的有序性是指元素的顺序和插入顺序一致,而TreeSet
的有序是指按照某种规则的排序
- 不允许添加重复元素。
- 支持根据元素顺序进行查找、遍历、子集操作等。
4.3自然排序
TreeSet 底层使用红黑树来存储元素,并使用元素的 compareTo() 方法进行比较。当向 TreeSet 中添加元素时,先将元素插入到红黑树的合适位置上,并标记节点为红色(因为红黑树中新增节点的默认颜色为红色)。随着元素的不断添加,红黑树可能会出现不平衡的情况,这时候就需要使用红黑树的自平衡机制进行调整。具体来说,红黑树使用节点颜色和旋转操作来保证树的平衡。
不懂吧,其实我也不懂,对红黑树不了解,等以后学习了数据结构再和大家来分享。
在这里,只要明白对于自然排序而言,会使用compareTo()方法进行比较。
package com.xqx.set; import java.util.Objects; import java.util.Set; import java.util.TreeSet; public class demo1 { public static void main(String[] args) { TreeSet s = new TreeSet(); s.add(new Student("zs", 16)); s.add(new Student("ww", 18)); s.add(new Student("ls", 17)); for (Object object : s) { System.out.println(object); } } } class Student implements Comparable { private String name; private int age; public Student(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; } @Override public boolean equals(Object o) { System.out.println("调用我心爱equals方法。。。"); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && name.equals(student.name); } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int hashCode() { System.out.println("调用我最爱的hashCode方法"); return Objects.hash(name, age); } @Override public int compareTo(Object o) { Student student = (Student) o; System.out.println(this.age + "aaa"); return this.age - student.age; } }
打印结果:
Student{name='zs', age=16} Student{name='ls', age=17} Student{name='ww', age=18}
4.4自定义排序
TreeSet 中的元素进行自定义排序,需要提供一个 Comparator 对象,也就是比较器,该对象实现了 Comparator 接口,并覆盖了 compare() 方法。compare() 方法可以实现自定义的比较逻辑,从而完成自定义排序。
package com.xqx.set; import java.util.Comparator; import java.util.Objects; import java.util.Set; import java.util.TreeSet; public class demo3 { public static void main(String[] args) { TreeSet ts = new TreeSet(); ts.add(new Student(1, "zs", 16)); ts.add(new Student(2, "ls", 17)); ts.add(new Student(3, "es", 16)); ts.add(new Student(4, "ww", 18)); System.out.println("=============默认id排序==============="); for (Object object : ts) { System.out.println(object); } TreeSet tsPlus = new TreeSet(new Comparator<Student>() { @Override public int compare(Student s1, Student s2) { if (s1.getAge() - s2.getAge() == 0) return s1.getName().compareTo(s2.getName()); return s1.getAge() - s2.getAge(); } }); for (Object object : ts) { tsPlus.add(object); } System.out.println("=============默认年龄排序,若年龄一致,按名字排序==============="); for (Object object : tsPlus) { System.out.println(object); } } } class Student implements Comparable<Student> { private int id; private String name; private int age; public int getId() { return id; } public void setId(int id) { this.id = id; } public Student(int id, String name, int age) { this.id = id; 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 int hashCode() { System.out.println("调用我心爱hashCode方法。。。"); final int prime = 31; int result = 1; result = prime * result + age; result = prime * result + id; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { System.out.println("调用我心爱equals方法。。。"); 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 (id != other.id) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public String toString() { return "Student [id=" + id + ", name=" + name + ", age=" + age + "]"; } @Override public int compareTo(Student o) { Student student = o; return this.id - student.id; } }
打印结果:
=============默认id排序=============== Student [id=1, name=zs, age=16] Student [id=2, name=ls, age=17] Student [id=3, name=es, age=16] Student [id=4, name=ww, age=18] =============默认年龄排序,若年龄一致,按名字排序=============== Student [id=3, name=es, age=16] Student [id=1, name=zs, age=16] Student [id=2, name=ls, age=17] Student [id=4, name=ww, age=18]
好啦,今天的分享就到此为止!希望你看完本篇文章有所收获,祝你变得更强!!!