HashSet工作原理

文章详细阐述了HashSet的初始化过程,包括默认容量、负载因子和扩容阈值。讨论了指定初始化容量对内存资源的影响,以及扩容的条件和扩容阈值的计算。同时,模拟实现了HashSet的添加、删除和扩容方法,并指出HashSet非线程安全,提供了规避线程安全问题的解决方案。最后,解释了在for循环中删除数据可能导致的ConcurrentModificationException异常及避免方法。
摘要由CSDN通过智能技术生成

目录

1.初始化做了哪些事儿?

2.第一次开辟的空间有多少?那初始化时指定开辟空间长度是否更有利于内存资源节省?

3.扩容是在什么时候扩的,达到的阈值是多少?初始申请的空间与扩容阈值之间的关系,围绕第二次扩容阈值与初始指定申请空间的关系说明?

4.模拟写一个新增或删除或扩容的方法

5.是否线程安全?为何不安全?如果不安全如何规避或替代类?

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异常。需要注意的是,如果需要删除的元素比较多,这种方式可能会占用较多的内存空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值