文章目录
Set接口简述
Set 接口是 Java 集合框架中的一种接口,它继承自 Collection 接口。Set 是一种无序、不允许重复元素的集合。
Set 接口的主要特点如下:
1.无序性:Set 中的元素没有特定的顺序,不保证元素的存储顺序和插入顺序一致。
2.唯一性:Set 中的元素不允许重复,每个元素在 Set 中只能出现一次。当想要确保集合中的元素唯一性时,可以使用 Set。
3.不可变性:Set 接口并没有提供修改集合元素的方法,即不能直接通过索引修改元素的值。如果想要修改 Set 中的元素,需要先删除原有元素,然后再添加新的元素。
4.常用实现类:Java 提供了几个常用的 Set 实现类,例如 HashSet、TreeSet 和 LinkedHashSet 等。
1.HashSet类
1.Hashset特点:
1.无序性
:HashSet 不保证元素的顺序,元素的存储和遍历顺序可能不一致。
2.不允许重复元素
:HashSet 不允许存储重复的元素,如果尝试插入重复元素,插入操作将被忽略。
3.允许 null 元素
:HashSet 允许存储 null 元素,但只能存储一个 null 元素。
2.Hashset的扩容策略
HashSet 是 Java 中的一个集合类,内部使用哈希表(HashMap)来实现。关于 HashSet 扩容的具体策略如下:
- 初始容量:在创建 HashSet 对象时,可以指定初始容量大小。如果没有指定,
默认初始容量为 16
。 - 负载因子:负载因子是表示哈希表在扩容之前可以达到多满的一个比例,默认为 0.75。例如,对于初始容量为 16 的HashSet,当元素个数达到 16 * 0.75 = 12 时,就会触发扩容操作。
- 扩容:当 HashSet的元素个数超过负载因子所允许的容量时,HashSet会自动进行扩容操作。扩容会创建一个更大容量的哈希表,并将旧表中的元素重新计算哈希码后插入到新表中。
- 扩容容量为原容量的两倍,即将容量
乘以2
。 - 扩容后,原来的所有元素会被
重新分配到新的位置
,这会导致一定的性能开销
- 扩容容量为原容量的两倍,即将容量
总结起来,HashSet 是在元素个数超过初始容量乘以负载因子时进行扩容操作,扩容容量为原容量的两倍。这样的扩容策略可以保持 HashSet 在元素数量逐渐增加时的性能较稳定。
3.Hashset的底层逻辑:
- HashSet的底层实现是基于哈希表(HashTable)的数据结构。具体来说,它使用了HashMap作为内部实现。
- HashSet通过HashMap来存储元素,其中HashSet的元素被看作是HashMap的键(
key
),而所有的键的值都被设置为一个常量(PRESENT
或者null
)。这意味着HashSet实际上是对HashMap的一种封装,只使用了HashMap中的键而没有使用值。 - 在HashSet中添加元素时,元素会经过哈希函数计算得到一个哈希码,然后根据哈希码找到对应的桶,如果桶中已经存在相同哈希码的元素,则会检查元素的
equals
方法是否返回true,如果返回true,则判断元素已经存在,不会重复添加;如果返回false,则将元素作为新的键添加到HashMap中。 - 因此,HashSet的底层实现可以认为是一个HashMap,其中元素的值被忽略,只使用HashMap的键来存储元素,并且确保元素在HashSet中的唯一性
4.hashset中哈希桶的存储
在 Java 的 HashSet 中,每个哈希桶(bucket)使用链表或红黑树来存储元素。当多个元素的哈希码映射到同一个桶时,这些元素会以链表形式存储在该桶中。
具体地说,当在 HashSet 中插入一个元素时,会根据元素的哈希码找到对应的桶。如果该桶为空,则直接将元素插入其中。如果该桶不为空,则需要进行以下操作:
- 遍历链表:遍历该桶中已存在的元素,比较这些元素与要插入的元素是否相等。如果找到相等的元素,则不进行插入操作,避免元素重复。
- 插入新节点:如果链表中不存在与要插入的元素相等的节点,则将新元素以节点的形式插入到链表的末尾。
这样,当多个元素的哈希码映射到同一个桶时,它们会以链表的形式连接在一起,通过链表中的节点进行存储和查找操作。
需要注意的是,为了提高查询效率,当链表中的元素数量达到一定阈值时,HashSet 会将链表转换为红黑树。这是为了在元素数量较大时,仍然能够保持较快的查找速度。当链表转换为红黑树后,与链表相比,查找、插入和删除的时间复杂度将从 O(n) 降低到 O(log n)。
图中这个数组0,1,2,3…就代表一个又一个的哈希桶
简单的示例
//下面不同的字符串就有相同的hashCode
System.out.println("ABCDEa123abc".hashCode()); // 165374702
System.out.println("ABCDFB123abc".hashCode()); // 165374702
可以看到这两个相同hashcode的字符串,但是equals比较之后不相同的话,就形成链表。
当然,我们也可以通过重写添加元素的hashcode()
与equals()
方法来达到自己想要的结果
1. 重写hashCode和equals原则
(1) hashCode
- 在程序运行时,同一个对象多次调用
hashCode()
方法应该返回相同的值。 - 当两个对象的
equals()
方法比较返回 true 时,这两个对象的hashCode()
方法的返回值也应相等。 - 对象中用作
equals()
方法比较的 Field,都应该用来计算 hashCode 值。
(2) equals - 当一个类有自己特有的“逻辑相等”概念,当改写
equals()
的时候,总是要改写hashCode()
,根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是, 根据Object.hashCode()
方法,它们仅仅是两个对象。 - 因此,违反了“相等的对象必须具有相等的散列码”。
- 结论:复写equals方法的时候一般都需要同时复写hashCode方法。 通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。
2.重写hashCode和equals方法的案例实现
首先我要说的是往hashset里面添加,几乎可以添加任何类型的元素,包括基本类型和对象
- 1.对于基本类型的元素,Java 会自动将其封装为对应的包装类。例如,可以将 int 类型的元素添加到 HashSet 中,实际上是将其封装为 Integer 类型的对象后进行操作。
- 2.需要注意的是,如果你想自定义类型的对象能够正确地在 HashSet 中运作,你需要确保重写了该类型的 hashCode() 和 equals() 方法。
这样才能正确地判断元素的唯一性
,并通过哈希码将其放入正确的桶中。
User类
public class User1![请添加图片描述](https://img-blog.csdnimg.cn/29f9ce3adff345c88ee99fcb87717c8a.png)
{
private String name;
private int age;
public User1() {
}
public User1(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 String toString() {
return "User1{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
System.out.println("User equals()....");
// return super.equals(o);
if (this == o) return true; //内存地址相同返回true
if (o == null || getClass() != o.getClass()) return false;
User1 user = (User1) o;
if (age != user.age) return false;
return name != null ? name.equals(user.name) : user.name == name;
}
/**
* * 注:**为什么hashCode中,用31这个数子?
* *
* * - 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
* * - 并且31只占用5bits,相乘造成数据溢出的概率较小。
* * - 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。 (提高算法效率)
* * - 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除! (减少冲突)
* @return
*/
@Override
public int hashCode() { //return name.hashCode() + age;
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
}
测试类
/**
* HashSet存入数据的过程
* 1. 得到对象的hashCode,hashCode%16,决定落到那个槽位(默认16个槽位)Entry[16]
* 2. 再次添加对象,如果对象的hashCode和第一次添加的相同,执行equals
* 3. 如果equals返回true,hashCode相同,equals相同,添加失败
* 4. 如果eques返回false,hashCode相同,形成一个链表
*
*
*/
@Test
public static void main(String[] args) {
Set set = new HashSet();
User1 u1 = new User1("马云",45);
User1 u2 = new User1("马云",45);
User1 u3 = new User1("马化腾",40);
User1 u4 = new User1("马化腾",40);
User1 u5 = new User1("奥巴马", 50);
User1 u6 = new User1("马云",45);
set.add(u1);
set.add(u2);
set.add(u3);
set.add(u4);
set.add(u5);
set.add(u6);
for (Object o : set) {
System.out.println(o);
}
}
这里没有形成链表,因为重写中我们把equals中名字跟age相同的就认为同一个值就不允许添加。
如果不重写equals
方法的话,本来User类的equals
比较的就是地址,如果地址相同就会认为是一样的,那么就会把相同姓名和相同年龄的都添加进去就不符合set集合 唯一值
的要求
3.哈希桶的链表到红黑树转换过程解析
还是上一段代码,不过鉴于形成红黑树的条件不容易找,我们就把上一段代码的重写equals方法去掉,让他们形成相同的hashcode
,但是调用equals比较地址来认为他们是不同的值
改变的代码如下:
public static void main(String[] args) {
Set set = new HashSet();
User1 u1 = new User1("马云",45);
User1 u2 = new User1("马云",45);
User1 u3 = new User1("马云",45);
User1 u4 = new User1("马云",45);
User1 u5 = new User1("马云",45);
User1 u6 = new User1("马云",45);
User1 u7 = new User1("马云",45);
User1 u8 = new User1("马云",45);
User1 u9 = new User1("马云",45);
User1 u10 = new User1("马云",45);
User1 u11 = new User1("马云",45);
User1 u12 = new User1("马云",45);
set.add(u1);
set.add(u2);
set.add(u3);
set.add(u4);
set.add(u5);
set.add(u6);
set.add(u7);
set.add(u8);
set.add(u9);
set.add(u10);
set.add(u11);
set.add(u12);
for (Object o : set) {
System.out.println(o);
}
}
user1:
private String name;
private int age;
public User1() {
}
public User1(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 String toString() {
return "User1{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int hashCode() { //return name.hashCode() + age;
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
接下来我们看一下过程
1.最开始:执行完 Set set = new HashSet();
语句
2.执行 set.add(u1);
我们可以看到这个值分到了第五个哈希桶,而且通过第二张图可知没有形成链表
3.执行完 set.add(u2);
我们可以看到通过我们自定义hashcode 和equals方法 我们可以让值分到一个哈希桶里
4.然后我们一直添加值,它就一直往第五个哈希桶里添加,增长链表的长度阈值为8
当第九次添加时候 会对table进行扩容 增加node数量 从 16增加到32
这时候链表还在第五个哈希桶中
5.第十次添加时候,因为链表长度阈值为8,但是如果想要让哈希桶里的链表形成红黑树,table的长度就至少要64,所以第十次添加的时候,table还会进行第二次扩容正好翻倍到64
每次扩容都要重新排哈希桶来放数据,所以这组暂时的链表
就被搬到了第37个哈希桶中
6.第十一次添加时:因为哈希桶里的链表形成红黑树的条件都已经达到了,所以这里链表就变成了红黑树
这里我认为在table中存储的node节点就会变成红黑树中的根节点,如有错误,欢迎指正
2.LinkedHashset
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
- LinkedHashSet插入性能略低于 HashSet, 但在迭代访问 Set 里的全部元素时有很好的性能。
- LinkedHashSet 不允许集合元素重复。
- LinkedHashSet 底层结构
3.Treeset
TreeSet 是 Java 中的一种有序集合
,它基于红黑树实现
。红黑树是一种自平衡的二叉搜索树,通过对元素进行排序并维护树的平衡,使得所有元素按照特定顺序排列,查询速度比List快
。TreeSet 是 SortedSet 接口的实现类, TreeSet 可以确保集合元素处于排序状态。而且不能有重复
,
新增的方法如下:
- Comparator comparator():返回定制排序器
- Object first():返回此集合中当前的第一个(最低)元素。
- Object last():返回此集合中当前的最后一个(最高)元素。
- Object lower(Object e):返回此集合中的最大元素严格小于给定元素,如果没有这样的元素,则 null 。
- Object higher(Object e)
- SortedSet subSet(fromElement, toElement)
- SortedSet headSet(toElement)
- SortedSet tailSet(fromElement)
TreeSet的两总排序方式
1. 自然排序
默认情况下, TreeSet 采用自然排序。
- 自然排序: TreeSet 会调用集合元素的 compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
- 如果试图把一个对象添加到 TreeSet时,则该对象的类必须实现 Comparable接口。
- 实现 Comparable接口 的类必须实现 compareTo(Objectobj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
- 0:相等,添加失败
- -1:小,放到左子树
- 1:大,放到右子树
Comparable接口的典型实现举例:
- BigDecimal、 BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
- Character:按字符的 unicode值来进行比较
- Boolean: true 对应的包装类实例大于 false 对应的包装类实例
- String:按字符串中字符的 unicode 值进行比较
- Date、 Time:后边的时间、日期比前面的时间、日期大
案例(自定义对象的添加):
user类
public class User implements Comparable {
private String name;
private int age;
public User() {
}
public User(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 String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//按照姓名从大到小排列,年龄从小到大排列
@Override
public int compareTo(Object o) {
if(o instanceof User){
User user = (User)o;
// return -this.name.compareTo(user.name);
// int compare = -this.name.compareTo(user.name);
int compare = this.name.compareTo(user.name);
if(compare != 0){
return compare;
}else{
return Integer.compare(this.age,user.age);
}
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
}
测试代码:
public class TreeSetDemo1 {
@Test
public void test1() {
System.out.println("cc".compareTo("ccc"));
// System.out.println(Integer.compare(3, 2));
}
public static void main(String[] args) {
User u1 = new User("aaa", 35);
User u2 = new User("bbb", 36);
User u3 = new User("ccc", 83);
User u4 = new User("ccc", 30);
User u5 = new User("ccc", 74);
User u6 = new User("eee", 39);
User u7 = new User("fff", 40);
User u8 = new User("ggg", 40);
User u9 = new User("ggg", 40);
TreeSet set = new TreeSet();
set.add(u1);
set.add(u2);
set.add(u3);
set.add(u4);
set.add(u5);
set.add(u6);
set.add(u7);
set.add(u8);
set.add(u9);
for (Object o : set) {
System.out.println(o);
}
}
}
需要注意的是:
- 向TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
- 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
- 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值。
- 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。否则,让人难以理解。
2.定制排序
- TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。 需要重写compare(T o1,T o2)方法。
- 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
- 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
- 此时, 仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
- 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。
- 定制排序器方法compare返回值
- 0:相等,添加失败
- -1:小,放到左子树
- 1:大,放到右子树
案例:
public class TreeSetDemo1 {
public static void main(String[] args) {
User u1 = new User("aaa", 35);
User u2 = new User("bbb", 35);
User u3 = new User("ccc", 35);
User u4 = new User("ccc", 30);
User u5 = new User("ccc", 74);
User u6 = new User("eee", 39);
User u7 = new User("fff", 3);
User u8 = new User("ggg", 2);
User u9 = new User("ggg", 1);
//定制排序器,匿名内部类
Comparator com = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
User u1 = (User) o1;
User u2 = (User) o2;
// return -Integer.compare(u1.getAge(), u2.getAge());
return u1.getName().compareTo(u2.getName());
}
};
TreeSet set = new TreeSet(com);
set.add(u1);
set.add(u2);
set.add(u3);
set.add(u4);
set.add(u5);
set.add(u6);
set.add(u7);
set.add(u8);
set.add(u9);
for (Object o : set) {
System.out.println(o);
}
}
}
总结
Set与List的差别
Set 和 List 都是 Java 中的集合接口,它们在以下几个方面有所差别:
- 元素的顺序:List 是有序集合,可以按照元素添加的顺序访问和获取元素。而 Set 是无序集合,不保证元素的顺序,不能通过索引访问元素。
- 元素的唯一性:List 允许存储重复元素,可以通过索引访问和修改相同值的元素。而 Set不允许存储重复元素,每个元素在集合中只能出现一次。
- 接口的实现类:Java 提供了多个 List 和 Set 的实现类。常见的 List 实现类有
ArrayList
、LinkedList
等;常见的 Set 实现类有HashSet
、TreeSet
、LinkedHashSet
等。 - 查找和插入性能:由于 List 可以根据索引访问元素,因此在遍历和随机访问元素方面具有较好的性能。而 Set在判断元素是否存在和插入新元素方面具有较好的性能,它使用哈希表或平衡树等数据结构来实现高效的查找和插入操作。
- Iterator 的顺序:使用
Iterator
迭代器遍历 List 返回的元素是按照插入顺序的。而 Set
返回的元素顺序可能是任意的,取决于具体的实现类。
选择使用 List 还是 Set 取决于你的具体需求。如果你需要按照插入顺序存储并访问元素,并且允许重复元素出现,则选择 List;如果你需要存储唯一的元素,并且不关心元素的顺序,则选择 Set。
注意,无论使用 List 还是 Set,都应该根据具体的情况选择适合的实现类,以获得更好的性能和功能。