HashSet及其实现原理
目录
- 一、Set
- 二、HashSet
- 三、HashSet的实现原理
- 四、HashSet的线程安全与顺序
- 1、线程安全
- 2、有序性
一、Set
Set 接口是 java.util 包下的一个集合接口,它继承自 Collection 接口。Set 接口定义了一个不允许包含重复元素的集合。Set 接口的实现类主要有 HashSet、LinkedHashSet 和 TreeSet。
以下是 Set 接口的一些关键特性:
- 不包含重复元素:Set 接口的实现类不允许集合中存在两个相等的对象。
- 无序性:大多数 Set 实现(如 HashSet)不保证集合的迭代顺序;特别是它不保证该顺序恒久不变。
- 唯一性:Set 接口的 add 方法在添加元素时,如果集合中已经存在该元素,则不会再次添加。
- 可选的排序:某些 Set 实现(如 TreeSet)会根据其元素的自然顺序或者构造集合时所指定的比较器来对元素进行排序。
关键特性总结:无序不重复、可选排序
Set 接口定义了以下方法:
- add(E e):添加元素,如果元素已存在则返回 false,否则添加后返回 true。
- remove(Object o):如果集合中存在该元素,则移除它并返回 true,否则返回 false。
- contains(Object o):检查集合是否包含指定元素。
- size():返回集合中的元素数量。
- isEmpty():检查集合是否为空。
- clear():移除集合中的所有元素。
- iterator():返回集合的迭代器。
二、HashSet
HashSet 是 Java 集合框架中的一个非常常用的集合类,它是 java.util包的一部分。HashSet继承自 AbstractSet 类,实现了 Set接口。底层基于HashMap实现。只存储元素,不存储键值对,它不允许重复的元素。
private transient HashMap<E,Object> map;// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();/*** Constructs a new, empty set; the backing <tt>HashMap</tt> instance has* default initial capacity (16) and load factor (0.75).*/
public HashSet() {map = new HashMap<>();
}
以下是 HashSet
的一些关键特性:
- 不允许重复:
HashSet
不允许集合中有重复的元素。如果尝试添加一个已经存在的元素,HashSet
会保持原样,不会添加新元素。 - 无序:
HashSet
不保证元素的顺序,每次迭代集合时元素的顺序可能不同。 - 基于哈希表:
HashSet
内部使用HashMap
实现,每个元素都作为HashMap
的一个键,而对应的值是一个虚拟对象(通常是HashSet
内部的一个静态对象)。 - 性能:由于
HashSet
基于哈希表,因此它在添加、删除和查找元素时通常提供常数时间的性能(即 O(1) 时间复杂度),但这取决于哈希函数的质量和冲突的数量。 - 不保证线程安全:
HashSet
不是线程安全的。如果需要线程安全的集合,可以使用Collections.synchronizedSet(new HashSet<>())
包装HashSet
,或者使用ConcurrentHashMap
的键集。 - 可以包含
null
元素:HashSet
允许包含一个null
元素,因为HashMap
允许null
作为键。 - 迭代器:
HashSet
提供迭代器来遍历集合中的所有元素。
主要特性总结: 无序不重复,只存储元素不存储键值对,线程不安全。
三、HashSet的实现原理
HashSet 是 Java 中的一个集合类,它基于 HashMap实现。下面是 HashSet的实现原理:
-
基于 HashMap:
- HashSet 的内部实际上是使用一个
HashMap
来存储元素的。 - 每个 HashSet 元素都作为
HashMap
的键,而对应的值是一个固定的虚拟对象(通常是一个静态的布尔值)。
- HashSet 的内部实际上是使用一个
-
元素唯一性:
- 由于
HashMap
的键是唯一的,所以 HashSet 能够保证元素的唯一性。
- 由于
-
添加元素:
-
当你向 HashSet添加一个元素时,它实际上是将该元素作为键添加到内部的
HashMap
中。 -
如果
HashMap
中已经存在这个键,则HashSet
会认为元素已经存在,不会添加重复的元素。public boolean add(E e) {return map.put(e, PRESENT)==null; }
-
-
删除元素:
- 当你从
HashSet
删除一个元素时,它实际上是从内部的HashMap
中删除对应的键。
- 当你从
-
查找元素:
- 当你检查
HashSet
是否包含某个元素时,它实际上是在内部的HashMap
中查找这个键。
- 当你检查
-
迭代器:
-
HashSet
的迭代器实际上是迭代内部HashMap
的键集。public Iterator<E> iterator() {return map.keySet().iterator(); }
-
-
性能:
HashSet
的添加、删除和查找操作的性能通常都是常数时间的(O(1)),这得益于HashMap
的性能。
-
容量和加载因子:
HashSet
继承了HashMap
的容量和加载因子的概念,这些参数影响HashSet
的性能和内存使用。
-
并发性:
- HashSet不是线程安全的。如果你需要线程安全的集合,可以使用
Collections.synchronizedSet(new HashSet<>(...))
或者ConcurrentHashMap
的键集。
- HashSet不是线程安全的。如果你需要线程安全的集合,可以使用
-
序列化:
- HashSet 实现了
Serializable
接口,这意味着它可以被序列化和反序列化。
- HashSet 实现了
四、HashSet的线程安全与顺序
1、线程安全
HashSet 是非线程安全的。这意味着多个线程同时修改 HashSet可能会导致不可预知的行为。如果多个线程需要访问和修改同一个 HashSet实例,必须通过外部同步来确保线程安全。
如果你需要线程安全的 Set
集合,可以考虑以下替代方案:
-
Collections.synchronizedSet:通过这个静态方法包装一个 HashSet,可以提供简单的线程安全。但是,这种方法的并发性能较低,因为每次访问都需要获取锁。
-
ConcurrentHashMap 的
keySet
方法:ConcurrentHashMap
提供了keySet
方法,它返回一个线程安全的Set
视图。这个Set
支持更高效的并发访问。 -
CopyOnWriteArraySet:这是一个线程安全的
Set
实现,它在每次修改(添加或删除元素)时都会复制整个底层数组。这种方法适用于读多写少的场景。import java.util.Collections; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet;public class ThreadSafeSetExample {public static void main(String[] args) {// 使用 Collections.synchronizedSet 包装 HashSetSet<String> syncSet = Collections.synchronizedSet(new HashSet<>());syncSet.add("Element1");syncSet.add("Element2");// 使用 CopyOnWriteArraySetSet<String> copyOnWriteSet = new CopyOnWriteArraySet<>();copyOnWriteSet.add("Element1");copyOnWriteSet.add("Element2");// 迭代两个线程安全的 SetSystem.out.println("Synchronized Set: " + syncSet);System.out.println("CopyOnWriteArraySet: " + copyOnWriteSet);} }
2、有序性
HashSet 不保证元素的顺序。它基于 HashMap实现,而 HashMap不保证键的顺序。因此,每次迭代 HashSet 时元素的顺序可能不同。
如果你需要一个有序的 Set
集合,你可以考虑以下几种方法:
-
TreeSet
: TreeSet 是一个有序的 Set集合,它基于红黑树实现。TreeSet可以确保元素处于排序状态,无论是按照自然顺序还是根据提供的 Comparator。 -
LinkedHashSet
: LinkedHashSet维护了元素的插入顺序,或者在构造时指定的最近期访问顺序。它内部使用 LinkedList 来维护元素的顺序,同时使用 HashMap 来保证元素的唯一性。 -
使用
HashSet
与额外的数据结构: 如果你需要保留 HashSet 的所有特性,并且只需要偶尔的顺序访问,你可以在需要的时候将 HashSet 元素添加到一个 TreeSet或 LinkedHashSet 中,进行排序操作。import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors;public class OrderedSetExample {public static void main(String[] args) {Set<Integer> hashSet = new HashSet<>();hashSet.add(5);hashSet.add(1);hashSet.add(4);hashSet.add(2);// 仅在需要有序时进行排序Set<Integer> sortedSet = hashSet.stream().collect(Collectors.toCollection(TreeSet::new));for (Integer number : sortedSet) {System.out.println(number);}} }
-
自定义有序 Set实现: 如果你有特殊的排序需求,你可以创建自己的
Set
实现,继承AbstractSet
类,并使用你选择的任何数据结构(如数组、链表等)来维护元素的顺序。