目录
2.第一次开辟的空间有多少?那初始化时指定开辟空间长度是否更有利于内存资源节省?
3.扩容是在什么时候扩的,达到的阈值是多少?初始申请的空间与扩容阈值之间的关系,围绕第二次扩容阈值与初始指定申请空间的关系说明?
6.for循环数据过程中删除数据是否异常?如果异常,寻找为何异常?如何规避这个异常?
1.初始化做了哪些事儿?
(1)当HashSet初始化时,会创建一个空的哈希表和一个默认容量为16的数组,同时设置负载因子为0.75和阈值为容量乘以负载因子。
(2)负载因子用于控制哈希表的填充程度,当哈希表中的元素数量达到负载因子(0.75)乘以容量(16)时,哈希表会自动扩容,容量会翻倍为32。
(3)阈值是在哈希表扩容时计算的,是容量乘以负载因子,用于判断是否需要扩容。
(4)同时,还会初始化一些常用方法,如添加元素、删除元素、判断元素是否存在等。
2.第一次开辟的空间有多少?那初始化时指定开辟空间长度是否更有利于内存资源节省?
(1)HashSet初始化时会创建一个默认容量为16的数组,不会指定开辟空间长度。如果需要指定初始容量,可以使用带有int参数的构造函数。例如,new HashSet<>(100)将创建一个初始容量为100的HashSet。
(2)指定初始容量可能会更有利于内存资源节省,因为它可以避免在添加元素时进行频繁的扩容操作。但是,如果初始容量过大,会浪费内存资源,因此需要根据实际需求来选择初始容量。
3.扩容是在什么时候扩的,达到的阈值是多少?初始申请的空间与扩容阈值之间的关系,围绕第二次扩容阈值与初始指定申请空间的关系说明?
(1)当HashSet中元素数量达到当前容量乘以负载因子时,就会触发扩容操作。默认情况下,容量为16,负载因子为0.75,因此当集合中的元素数量达到12时,就会自动扩容。
(2)初始申请的空间决定了HashSet的初始容量,扩容阈值取决于初始容量和负载因子的乘积。第二次扩容阈值取决于第一次扩容后的容量和负载因子的乘积。初始申请的空间越大,HashSet的扩容次数就越少,但是也会浪费更多的内存空间。
4.模拟写一个新增或删除或扩容的方法
import java.util.Arrays;
public class MyHashSet<E> {
private static final int DEFAULT_CAPACITY = 16; // 默认容量
private static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认装载因子
private int size; // 元素个数
private int capacity; // 容量
private float loadFactor; // 装载因子
private Node<E>[] table; // 存储元素的数组
public MyHashSet() {
this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public MyHashSet(int capacity, float loadFactor) {
this.capacity = capacity;
this.loadFactor = loadFactor;
this.table = new Node[capacity];
}
public boolean add(E element) {
if (contains(element)) {
return false; // 如果元素已存在,直接返回false
}
if (size >= capacity * loadFactor) {
resize(); // 如果需要扩容,进行扩容操作
}
int index = getIndex(element); // 计算元素在数组中的索引
Node<E> newNode = new Node<>(element); // 创建新节点
newNode.next = table[index]; // 将新节点插入到链表头部
table[index] = newNode;
size++;
return true;
}
public boolean remove(E element) {
int index = getIndex(element); // 计算元素在数组中的索引
Node<E> prev = null;
Node<E> current = table[index];
while (current != null) { // 遍历链表
if (current.element.equals(element)) { // 如果找到了需要删除的元素
if (prev == null) { // 如果需要删除的元素位于链表头部
table[index] = current.next;
} else { // 如果需要删除的元素位于链表中间或尾部
prev.next = current.next;
}
size--;
return true;
}
prev = current;
current = current.next;
}
return false; // 如果没有找到需要删除的元素,返回false
}
private void resize() {
capacity *= 2; // 容量扩大一倍
Node<E>[] oldTable = table; // 保存旧数组
table = new Node[capacity]; // 创建新数组
for (Node<E> node : oldTable) { // 遍历旧数组中的每个节点
while (node != null) {
Node<E> next = node.next;
int index = getIndex(node.element); // 计算节点在新数组中的索引
node.next = table[index]; // 将节点插入到链表头部
table[index] = node;
node = next;
}
}
}
private int getIndex(E element) {
int hashCode = element.hashCode(); // 计算元素的哈希值
return hashCode % capacity; // 取模运算,得到元素在数组中的索引
}
public boolean contains(E element) {
int index = getIndex(element); // 计算元素在数组中的索引
Node<E> current = table[index];
while (current != null) { // 遍历链表
if (current.element.equals(element)) { // 如果找到了元素
return true;
}
current = current.next;
}
return false; // 如果没有找到元素,返回false
}
public int size() {
return size;
}
private static class Node<E> {
E element; // 节点存储的元素
Node<E> next; // 下一个节点的引用
public Node(E element) {
this.element = element;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (Node<E> node : table) { // 遍历数组中的每个节点
while (node != null) { // 遍历链表
sb.append(node.element); // 将元素添加到字符串中
sb.append(", ");
node = node.next;
}
}
if (sb.length() > 1) {
sb.setLength(sb.length() - 2); // 去掉最后一个逗号和空格
}
sb.append("]");
return sb.toString(); // 将字符串返回
}
}
add方法和remove方法使用了链表法解决哈希冲突,resize方法用于扩容,getIndex方法用于计算元素在数组中的索引,contains方法用于判断元素是否存在,size方法用于返回元素个数,Node类用于表示链表节点,toString方法用于将元素转换为字符串。
5.是否线程安全?为何不安全?如果不安全如何规避或替代类?
(1)HashSet不是线程安全的,因为它的内部结构不是线程安全的。如果多个线程同时修改HashSet,可能会导致数据不一致或抛出异常。
(2)要规避HashSet的线程安全问题,可以使用Collections类提供的synchronizedSet方法将HashSet转换为线程安全的集合。例如,可以使用如下代码创建一个线程安全的HashSet:
Set<String> synchronizedSet = Collections.synchronizedSet(new HashSet<String>());
(3)另一种替代类是ConcurrentHashMap,它是线程安全的HashMap实现。ConcurrentHashMap的内部结构使用了锁分离技术,可以支持多个线程同时访问和修改集合,因此适用于高并发环境。但是,ConcurrentHashMap的性能可能会比HashSet略差一些,具体情况需要根据实际需求进行选择。
6.for循环数据过程中删除数据是否异常?如果异常,寻找为何异常?如何规避这个异常?
在for循环数据过程中删除数据可能会引发ConcurrentModificationException异常,因为在遍历过程中删除元素会改变集合的结构,从而导致遍历器无法正常工作。
例如,以下代码会在第二次循环时抛出ConcurrentModificationException异常:
Set<String> set = new HashSet<>(Arrays.asList("a", "b", "c"));
for (String s : set) {
if (s.equals("b")) {
set.remove(s);
}
}
这是因为在第二次循环时,集合的结构已经发生了改变,但是遍历器并不知道这个改变,因此会抛出异常。
(1)要规避这个异常,可以使用迭代器的remove方法来删除元素,而不是使用集合的remove方法。例如,以下代码可以正常遍历并删除元素:
1.迭代器删除
Set<String> set = new HashSet<>(Arrays.asList("a", "b", "c"));
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("b")) {
iterator.remove();
}
}
使用迭代器的remove方法可以保证在遍历过程中安全地删除元素,从而避免ConcurrentModificationException异常。
2.复制到另一个集合将其全部删除
(2)还有一种方式可以安全地删除HashSet中的元素,那就是先将需要删除的元素放到一个临时集合中,遍历完成后再将这些元素从原始HashSet中删除。例如,以下代码可以安全地删除HashSet中的元素:
Set<String> set = new HashSet<>(Arrays.asList("a", "b", "c"));
Set<String> toRemove = new HashSet<>();
for (String s : set) {
if (s.equals("b")) {
toRemove.add(s);
}
}
set.removeAll(toRemove);
这种方式相比使用迭代器的remove方法,代码复杂度更高,但是可以保证在遍历过程中安全地删除元素,从而避免ConcurrentModificationException异常。需要注意的是,如果需要删除的元素比较多,这种方式可能会占用较多的内存空间。