跳至主要內容

06. HashSet

LiuSongLing大约 4 分钟javajavalist

本节探讨另一种集合数据结构,HashSet,最流行的 Set 实现之一。

1.HashSet简介

HashSet 是 Java Collections API 中的基本数据结构之一.

让我们了解一下它重要的点:

  • 它存储元素唯一并允许 null;
  • 它由 HashMap 提供支持;
  • 它不维持插入顺序;
  • 它不是线程安全的。

请注意,这个内部 HashMap 在创建 HashSet 的实例时被初始化:

public HashSet() {
    map = new HashMap<>();
}

2.添加

add() 方法可用于将元素添加到集合中。方法协定规定,仅当元素尚未存在于 set 中时,才会添加该元素。如果添加了元素,则该方法返回 true,否则返回 false 。

@Test
public void whenAddingElement_shouldAddElement() {
    Set<String> hashset = new HashSet<>();
 
    assertTrue(hashset.add("String Added"));
}

实现细节说明了 HashSet内部的工作原理,它利用了 HashMap的 put方法:

public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}

map 变量是对内部支持的 HashMap的引用:

private transient HashMap<E, Object> map;

3.包含

contains 方法的目的是检查给定的 HashSet 中是否存在元素。 如果找到元素,则返回 true,否则返回 false。

@Test
public void whenCheckingForElement_shouldSearchForElement() {
    Set<String> hashsetContains = new HashSet<>();
    hashsetContains.add("String Added");
 
    assertTrue(hashsetContains.contains("String Added"));
}

每当将对象传递给此方法时,都会计算哈希值。然后,解析并遍历相应的存储桶位置。

4.删除

该方法从集中删除指定的元素(如果存在)。如果集合包含指定的元素,则此方法返回 true。

@Test
public void whenRemovingElement_shouldRemoveElement() {
    Set<String> removeFromHashSet = new HashSet<>();
    removeFromHashSet.add("String Added");
 
    assertTrue(removeFromHashSet.remove("String Added"));
}

如果打算从 Set 中删除所有元素时,可以使用 清除方法:

@Test
public void whenClearingHashSet_shouldClearHashSet() {
    Set<String> clearHashSet = new HashSet<>();
    clearHashSet.add("String Added");
    clearHashSet.clear();
    
    assertTrue(clearHashSet.isEmpty());
}

5.迭代器

该方法返回 Set 中元素的迭代器。元素的访问没有特定的顺序,迭代器是快速失败的。

@Test
public void whenIteratingHashSet_shouldIterateHashSet() {
    Set<String> hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator<String> itr = hashset.iterator();
    while(itr.hasNext()){
        System.out.println(itr.next());
    }
}

如果在创建迭代器后的任何时间以除迭代器自己的 remove 方法以外的任何方式修改了该 Set 集合,则迭代器将引发 ConcurrentModificationException。

如果我们使用迭代器的 remove 方法,那么我们就不会遇到异常:

@Test
public void whenRemovingElementUsingIterator_shouldRemoveElement() {
 
    Set<String> hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator<String> itr = hashset.iterator();
    while (itr.hasNext()) {
        String element = itr.next();
        if (element.equals("Second"))
            itr.remove();
    }
 
    assertEquals(2, hashset.size());
}

6.唯一性

当我们将一个对象放入 HashSet 中时,它使用该对象的 hashcode 值来确定某个元素是否已经不在集合中。

每个哈希代码值对应于一个特定的存储桶位置,该位置可以包含各种元素,计算出的哈希值相同。但是具有相同 hashCode 的两个对象可能不相等。

因此,将使用 equals() 方法比较同一存储桶中的对象。

7.性能

HashSet 的性能主要受两个参数的影响——初始容量和负载因子。

向 set 添加元素的预期时间复杂度为 O(1),在最坏的情况下(只有一个存储桶)可能会下降到 O(n) —— 因此,保持正确的 HashSet 容量至关重要。

重要提示:从 JDK 8 开始,最坏情况下的时间复杂度是 O(log*n)。

负载系数描述了最大填充级别是多少,超过该级别,需要调整一组的大小。

我们还可以创建一个具有初始容量和负载因子自定义值的 HashSet:

Set<String> hashset = new HashSet<>();
Set<String> hashset = new HashSet<>(20);
Set<String> hashset = new HashSet<>(20, 0.5f);

在第一种情况下,使用默认值 – 初始容量 16 和负载系数 0.75。 在第二个版本中,我们覆盖默认容量, 在第三个版本中,我们覆盖这两个容量。

较低的初始容量会降低空间复杂性,但会增加重新哈希的频率,这是昂贵的代价。另一方面,较高的初始容量会增加迭代成本和初始内存消耗。

根据经验:

  • 高初始容量适用于大量条目,并且很少或没有迭代;
  • 较低的初始容量适用于具有大量迭代的几个条目。

因此,在两者之间取得正确的平衡非常重要。