天天看点

Java中HashSet那点事

1.概述

在本文中,我们将深入研究HashSet。它是最流行的Set实现之一,也是Java Collections Framework的一个组成部分。

2. HashSet简介

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

让我们回顾一下这个实现的最重要方面:

  • 它存储唯一元素并允许空值
  • 它由HashMap支持
  • 它不保持插入顺序
  • 它不是线程安全的

请注意,在创建HashSet的实例时,会初始化此内部HashMap:

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

3. API

在本节中,我们将回顾最常用的方法,并查看一些简单的示例。

3.1 add()

所述的add()方法可用于将元素添加到一组。方法声明只有当元素尚未存在于集合中时才会添加元素。如果成功添加了元素,则该方法返回true,否则返回false。

我们可以向HashSet添加一个元素,如:

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

从实现的角度来看,add方法是一个非常重要的方法。实现细节说明了HashSet如何在内部工作并利用HashMap的 put方法:

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

到内部w使用是Map变量:

private transient HashMap<E, Object> map;
           

最好先熟悉哈希码,以便详细了解基于哈希的数据结构中元素的组织方式。

总结:

  • 一个HashMap中是16个默认容量元素的阵列-每个区块对应于不同的哈希码值
  • 如果各种对象具有相同的哈希码值,则它们将存储在单个存储buckets
  • 如果达到了加载因子,则会创建一个新数组,该数组的大小是前一个数组的两倍,并且所有元素都会被重新分散并在新的相应存储块中并重新分配
  • 要检索一个值,我们对一个键进行哈希处理,对其进行修改,然后转到相应的存储块,并在存在多个对象的情况下搜索潜在的链表。
3.2 contains()

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

我们可以在HashSet中检查一个元素:

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

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

3.3 remove()

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

让我们看一个有效的例子:

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

当我们打算从集合中删除所有项目时,我们使用此方法。底层实现只是清除底层HashMap中的所有元素。

让我们看看行动:

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

这是API中的基本方法之一。它被大量使用,因为它有助于识别HashSet中存在的元素数量。底层实现只是将计算委托给HashMap的size()方法。

让我们看看行动:

@Test
public void whenCheckingTheSizeOfHashSet_shouldReturnThesize() {
    Set<String> hashSetSize = new HashSet<>();
    hashSetSize.add("String Added");
     
    assertEquals(1, hashSetSize.size());
}
           
3.6 isEmpty()

我们可以使用此方法来确定HashSet的给定实例是否为空。如果集合不包含任何元素,则此方法返回true:

@Test
public void whenCheckingForEmptyHashSet_shouldCheckForEmpty() {
    Set<String> emptyHashSet = new HashSet<>();
     
    assertTrue(emptyHashSet.isEmpty());
}
           
3.7 Iterator()

该方法返回Set中元素的迭代器。这些元素没有特定的顺序访问,Iterator是fail-fast的。

我们可以在这里观察随机迭代顺序:

@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方法删除元素,在创建迭代器之后的任何时间修改集合,则迭代器会抛出ConcurrentModificationException。

让我们看看行动:

@Test(expected = ConcurrentModificationException.class)
public void whenModifyingHashSetWhileIterating_shouldThrowException() {
  
    Set<String> hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator<String> itr = hashset.iterator();
    while (itr.hasNext()) {
        itr.next();
        hashset.remove("Second");
    }
}
           

或者,如果我们使用了迭代器的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());
}
           

迭代器的Iterator行为无法得到保证,因为在存在非同步并发修改的情况下无法做出任何硬性保证。

4. HashSet如何保持唯一性?

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

每个散列码值对应于某个块位置,该块位置可以包含计算出的散列值相同的各种元素。但是具有相同hashCode的两个对象可能不相等。

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

5. HashSet的性能

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

将元素添加到集合的预期时间复杂度是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。在第二个中,我们覆盖默认容量,在第三个中,我们覆盖两者。

较低的初始容量降低了空间复杂性,但增加了重新散布的频率,这是一个昂贵的过程。

另一方面,高初始容量增加了迭代成本和初始内存消耗。

根据经验:

  • 高初始容量适用于大量条目,几乎没有迭代
  • 低初始容量适用于具有大量迭代的少数条目

因此,在两者之间取得正确的平衡非常重要。通常,默认实现是优化的并且工作正常,如果我们觉得需要调整这些参数以满足要求,我们需要明智地做。

6.结论

在本文中,我们概述了HashSet的实用性,其目的以及它的基础工作。我们看到了它在可用性方面的效率,因为它具有恒定的时间性能和避免重复的能力。

我们研究了API中的一些重要方法,它们如何帮助我们作为开发人员使用HashSet来发挥其潜力。

Java中HashSet那点事
微信关注:Java知己, 每天更新Java知识哦,期待你的到来!
Java中HashSet那点事