一、Set
1.1、Set的基本知识
set也是单列集合的一种,用于存储一组不重复的元素。它是一种集合数据类型,常用于需要确保元素唯一性和快速查找的场景。他有如下特点:
- 无序性:Set 中的元素是无序的,没有特定的顺序。
- 唯一性:Set 中不允许有重复的元素。
- 高效性:典型的 Set 实现(如哈希集合)可以在平均情况下提供 O(1) 时间复杂度的添加、删除和查找操作。
- 无索引:Set当中没有索引,无法通过索引来操作数据。
1.2、Set集合的实现类
- Hashset:
无序
、不重复、无索引 - LinkedHashset:
有序
、不重复、无索引 - Treeset:
可排序
、不重复、无索引
set继承自collection,他的api与collection相似,来回忆一下collection都有什么成员方法:
public boolean add(E e) 把给定的对象添加到当前集合中。
public void clear() 清空集合中所有的元素。
public boolean remove(E e) 把给定的对象在当前集合中册除。
public boolean contains(E e) 判断当前集合中是否包合给定的对象。
public boolean isEmpty() 判断当前集合是否为空。
public int size() 返回集合中元素的个数。
public Object[] toArray() 把集合中的元素,存储到数组中。
二、HashSet
2.1、哈希表与哈希值
HashSet
是Set实现类的一种,其底层是基于哈希表
存储数据。
哈希表是一种对增删改查性能都良好的结构。
哈希表的组成:
- jdk8以前:数组+链表
- jdk8以后:数组+链表+红黑树
哈希表的灵魂是
哈希值
(可以理解为对象的整数表现形式),其特点如下:
- 根据hashcode方法算出来的int类型的整数
- 该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
- 一般情况下,会
重写hashcode方法
,利用对象内部的属性值
计算哈希值对象哈希值的特点:
- 如果
没有重写
hashcode方法,不同对象计算出的哈希值是不同的- 如果
已经重写
hashcode方法,不同的对象只要属性值相同
,计算出的哈希值就是一样的- 在小部分情况下,
不同的属性值或者不同的地址值计算出来的哈希值也有可能一样
。(哈希冲突)
没有重写:
class Person {String name;Person(String name) {this.name = name;}
}
public class Main {public static void main(String[] args) {Person person1 = new Person("Alice");Person person2 = new Person("Alice");// 没有重写 hashCode 方法,不同对象计算出的哈希值是不同的System.out.println(person1.hashCode()); // 366712642System.out.println(person2.hashCode()); // 1829164700}
}
重写hashcode方法(自己编的hash算法,正常开发IDEA有选项可以自动重写hashcode与equals):
class PersonWithHashCode {String name;PersonWithHashCode(String name) {this.name = name;}@Overridepublic int hashCode() {return name != null ? name.hashCode() : 0;}
}
public class Main {public static void main(String[] args) {PersonWithHashCode person1 = new PersonWithHashCode("Alice");PersonWithHashCode person2 = new PersonWithHashCode("Alice");// 重写 hashCode 方法,不同的对象只要属性值相同,计算出的哈希值就是一样的System.out.println(person1.hashCode()); // 63281965System.out.println(person2.hashCode()); // 63281965}
}
2.2、HashSet 的特性
如果集合中存储的是自定义对象,必须要重写hashcode和equals方法
- 唯一性:
HashSet
中的每个元素都是唯一的,不允许重复元素。 - 无序性:
HashSet
不保证元素的顺序,即元素的存储顺序和插入顺序可能不同。 - 高效性:
HashSet
的查找、插入和删除操作平均时间复杂度为 O(1)。
2.3、HashSet的底层
当我们敲出这行代码的时候:
HashSet<String> hashSet = new HashSet<>();
第一步
:他的底层会创建一个默认长度16
,默认加载因子为0.75
的数组,数组名table;
第二步
:根据元素的哈希值跟数组的长度计算出应存入的位置;
第三步
:判断存入的位置是否为null,如果为null直接存入;
第四步
:如果位置不为null,表示有元素,则调用equals方法比较属性值;
第五步
:如果一样则不存入,如果不一样则形成链表;
- JDK8以前:新元素存入数组,老元素挂在新元素下面
- JDK8以后:新元素直接挂在老元素下面
第六步
:当数组长度超过 默认长度 * 加载因子
(16 * 0.75=12)的时候,数组就会扩容到之前的两倍变成新的默认长度(32);
第七步
:在jdk8
以后,如果链表长度超过8
而且数组长度超过64
,则链表会转化为红黑树
;
索引4下方为链表:
长度超过了8,转化为红黑树:
2.3、三个小问题:
2.3.1、HashSet为什么存
和取
是无序的?
因为本身添加就是根据哈希值计算出来的地址,所以添加的时候顺序就是不固定的,但是取值的时候顺序是固定的,从数组索引0开始查找取值。
2.3.2、HashSet为什么没有索引?
因为HashSet不够纯粹,数组虽然有索引,但是数组元素下面任然挂着数据,无法确定索引。
2.3.3、HashSet利用什么来保证数据的去重的?
利用HashCode方法以及equals,通过HashCode算出哈希值,然后通过equals判断是否有相同的元素。
2.4、成员方法
方法 | 说明 |
---|---|
add(E e) | 将指定的元素添加到此集合中(如果尚未存在)。 |
clear() | 移除此集合中的所有元素。 |
contains(Object o) | 如果此集合包含指定的元素,则返回 true 。 |
isEmpty() | 如果此集合不包含元素,则返回 true 。 |
iterator() | 返回在此集合的元素上进行迭代的迭代器。 |
remove(Object o) | 如果指定的元素存在于此集合中,则将其移除。 |
size() | 返回此集合中的元素数量(集合的容量)。 |
clone() | 返回此 HashSet 实例的浅表副本:元素本身不被复制。 |
spliterator() | 创建一个 Spliterator 以按适当顺序遍历此集合中的元素。 |
toArray() | 返回包含此集合中所有元素的数组。 |
toArray(T[] a) | 返回包含此集合中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。 |
removeIf(Predicate<? super E> filter) | 移除此集合中满足给定谓词的所有元素。 |
stream() | 返回包含此集合中所有元素的顺序流。 |
parallelStream() | 返回可能并行的包含此集合中所有元素的流。 |
// 创建一个新的HashSet
HashSet<String> set = new HashSet<>();// add(E e) 方法示例
set.add("Apple");
set.add("Banana");
set.add("Cherry");// contains(Object o) 方法示例
System.out.println("Contains 'Apple': " + set.contains("Apple")); // true
System.out.println("Contains 'Mango': " + set.contains("Mango")); // false// isEmpty() 方法示例
System.out.println("Is empty: " + set.isEmpty()); // false// iterator() 方法示例
Iterator<String> iterator = set.iterator();
System.out.print("Iterator values: ");
while (iterator.hasNext()) {System.out.print(iterator.next() + " ");
}
System.out.println();// remove(Object o) 方法示例
set.remove("Banana");
System.out.println("After removal of 'Banana': " + set); // [Apple, Cherry]// size() 方法示例
System.out.println("Size: " + set.size()); // 2// clone() 方法示例
HashSet<String> clonedSet = (HashSet<String>) set.clone();
System.out.println("Cloned set: " + clonedSet); // [Apple, Cherry]// spliterator() 方法示例
System.out.println("Spliterator estimate size: " + set.spliterator().estimateSize()); // 2// toArray() 方法示例
Object[] array = set.toArray();
System.out.println("Array: " + java.util.Arrays.toString(array)); // [Apple, Cherry]// toArray(T[] a) 方法示例
String[] stringArray = new String[set.size()];
set.toArray(stringArray);
System.out.println("String array: " + java.util.Arrays.toString(stringArray)); // [Apple, Cherry]// removeIf(Predicate<? super E> filter) 方法示例
Predicate<String> filter = str -> str.startsWith("A");
set.removeIf(filter);
System.out.println("After removeIf (str -> str.startsWith(\"A\")): " + set); // [Cherry]// clear() 方法示例
set.clear();
System.out.println("Is empty after clear: " + set.isEmpty()); // true
三、LinkedHashSet
3.1、LinkedHashSet的基本知识
LinkedHashSet继承自HashSet,上面我们说到LinkedHashSet是有序,不可重复的,其实这里的有序指的是存和取的顺序是有序的。
LinkedHashSet
的底层数据结构是依然哈希表
,只是每个元素又额外的多了一个双链表
的机制记录存储的顺序。
索引8为第一个添加,索引3为第二个添加,当第二个添加完成后,索引8的值会记录索引3的地址值,索引3也会记录索引8的地址值。每一次添加,最后一个与倒数第二个都会相互记录地址值。
当LinkedHashSet遍历的时候,就会遍历双向链表,所以存和取的顺序就相同了。
3.2、成员方法
与HashSet差不多
方法 | 说明 |
---|---|
add(E e) | 将指定的元素添加到此集合中(如果尚未存在)。 |
clear() | 移除此集合中的所有元素。 |
contains(Object o) | 如果此集合包含指定的元素,则返回 true 。 |
isEmpty() | 如果此集合不包含元素,则返回 true 。 |
iterator() | 返回在此集合的元素上进行迭代的迭代器,按插入顺序迭代。 |
remove(Object o) | 如果指定的元素存在于此集合中,则将其移除。 |
size() | 返回此集合中的元素数量(集合的容量)。 |
clone() | 返回此 LinkedHashSet 实例的浅表副本:元素本身不被复制。 |
spliterator() | 创建一个 Spliterator 以按插入顺序遍历此集合中的元素。 |
toArray() | 返回包含此集合中所有元素的数组。 |
toArray(T[] a) | 返回包含此集合中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。 |
removeIf(Predicate<? super E> filter) | 移除此集合中满足给定谓词的所有元素。 |
stream() | 返回包含此集合中所有元素的顺序流。 |
parallelStream() | 返回可能并行的包含此集合中所有元素的流。 |