笔者在参与某银行面试时,被问到了这样的问题。HashSet的实现原理有了解过吗?由于这个小点平时只是使用,但是源码确实没看过于是就只能“囊中羞涩”了。
(以openjdk-19为例)
首先,我们要分析HashSet,就还要看其实现的接口Set本身包含哪些方法。
public interface Set<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Coolection<?> c);
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
@Override
default Spliterator<E> spliteraotr() {
return Spliterators.spliteraotr(thiss, spliterator.DISTINCT);
}
}
可以说,由于开发流程完善与历史积淀厚重,源码中的注释非常完备,我们已经可以从简称中了解到不同方法的目的,不过有些默认注解需要我们额外注重就是了,比如NotNull。
我们首先应该看源代码中的整体注释,这里就不放原文,感兴趣的可以自行翻阅。
HashSet底层源码中,有部分方法的入参均涉及loadFactor
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
该装填因子通常用来衡量哈希表的填充程度,具体来说其计算公式如下
装填因子 = 元素数量 / 哈希表容量
HashMap在扩容时,会根据装填因子的值判断是否需要进行扩容操作。上述源代码中的.75f即对应着默认阈值为0.75,这个是根据经验得到的平衡点,在内存占用和查找效率之间做出的权衡。
在Java开发中,“快速失败”(fail-fast)是一种迭代器(Iterator)或集合(Collection)的行为机制。当一个迭代器检测到在迭代过程中集合被修改时(除了通过迭代器自身的remove方法),它会立即抛出ConcurrentModificationException异常,以避免在未来的迭代中产生不确定的行为。
快速失败机制的目的是提前检测并报告并发修改,以确保程序能够在迭代过程中发现错误,而不是在后续的迭代中可能导致不确定的结果。这样可以帮助开发人员及早发现并修复潜在的并发问题,提高程序的可靠性和稳定性。
Java中并没有"慢失败"(slow-fail)的机制,如果Java使用慢失败机制,那么在迭代器遍历集合期间,如果集合被修改,迭代器可能会继续执行而不抛出异常,这可能导致迭代器在后续的操作中产生错误的结果。
HashSet、LinkedHashSet和TreeSet是Set接口的实现类,它们都具有保证元素唯一性的特性,并且都不是线程安全的。这三者之间的主要区别在于它们使用的底层数据结构不同。
HashSet使用哈希表作为底层数据结构,实际上是基于HashMap实现的。哈希表通过将元素的哈希码映射到桶(bucket)的索引来存储元素,并使用链表或红黑树来解决哈希冲突。HashSet适用于不需要保证元素插入和取出顺序的场景。由于哈希表的特性,HashSet提供了快速的添加、删除和查找操作,时间复杂度为O(1)。
LinkedHashSet底层数据结构包含了链表和哈希表。它继承自HashSet,并在HashSet的基础上通过使用链表来维护元素的插入顺序。具体而言,当元素被添加到LinkedHashSet时,它会被插入到链表的尾部,从而保证了元素的插入和取出顺序满足FIFO(先进先出)的特性。LinkedHashSet适用于需要保留元素插入顺序的场景。
TreeSet底层数据结构是红黑树(一种自平衡的二叉查找树)。它实现了SortedSet接口,可以确保元素按照特定的顺序进行排序。TreeSet支持自然排序(元素的自然顺序)和定制排序(通过Comparator接口定义的排序规则)。由于红黑树的特性,TreeSet中的元素是有序的。插入、删除和查找操作的时间复杂度是O(log N)。TreeSet适用于需要元素有序排列或自定义排序规则的场景。
以下是三种实现的源码片段,我们重点关注其使用的数据结构:
HashSet:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
private transient HashMap<E,Object> map;
// ...
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// ...
}
LinkedHashSet:
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, Serializable {
private transient LinkedHashMap<E,Object> map;
// ...
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// ...
}
TreeSet:
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable {
private transient NavigableMap<E,Object> map;
// ...
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// ...
}
上述源码片段展示了HashSet、LinkedHashSet和TreeSet的底层数据结构的具体实现,其中HashSet和LinkedHashSet使用了HashMap和LinkedHashMap作为底层数据结构,而TreeSet使用了NavigableMap(通常为TreeMap)作为底层数据结构。
我们在面试或者上机考试的手撕中,可以考虑借助TreeMap的key有序的特点来方便我们解决问题。