深入理解java之HashSet

深入理解java之HashSet

本文我们深入讨论HashSet,Set接口最常用的实现,也是java Collection Framework的一个组成部分。

HashSet简介

HashSet是java集合API中基础数据结构之一,我们回顾起实现中最基本的方面:

  • 存储唯一元素,允许null值
  • 基于HashMap实现
  • 不维护插入顺序
  • 不是线程安全的

注意,当创建HashSet实例时,内部HashMap被初始化:

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

如果你对HashMap感兴趣,可以阅读深入理解Java HashMap

HashSet API

本节我们通过一些简单示例说明HashSet最常用的方法。

add()

add方法往set中增加元素。该方法约定当set中不存在该元素时将增加,如果增加成功返回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;
}

HashMap类型的map变量为HashSet的内部引用变量。

private transient HashMap<E, Object> map;

首先熟悉hashcode并深入理解基于hash数据结构如何组织元素是非常有必要,这里简单总结如下:

  • HashMap基于内部数组(桶),缺省容量为16,每个桶对应不同的hashcode值。
  • 如果不同对象有相同的hashcode值,则这些对象被存储在相同的桶中。
  • 如果达到载入因子对应容量时,会创建大小为原来的两倍的新数组,所有元素需要重新计算哈希值,并重新分布相应的桶中。
  • 获取值,首先哈希key并修改对应哈希值,然后找到相应桶,如果桶中有多个对象,则需要搜索其中链接list并返回对应值。

contains()

contains()方法的目的是检查给定HashSet是否存储指定元素。如果找到返回true,反之为false。示例代码:

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

    assertTrue(hashsetContains.contains("String Added"));
}

当给这个方法传递参数对象时,哈希值就会被计算出来。然后在相应的bucket位置解析和遍历相应的值。

remove()

remove()方法从set中删除指定元素。如果set包含指定元素返回true。示例如下:

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

    assertTrue(removeFromHashSet.remove("String Added"));
}

clear()

如果我们打算删除set中所有元素,可以使用该方法。底层实现简单清除内部HashMap中所有元素。示例代码:

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

    assertTrue(clearHashSet.isEmpty());
}

size()

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

@Test
public void whenCheckingTheSizeOfHashSet_shouldReturnThesize() {
    Set<String> hashSetSize = new HashSet<>();
    hashSetSize.add("String Added");

    assertEquals(1, hashSetSize.size());
}

iterator()

该方法返回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());
    }
}

set的迭代器被创建之后,如果set被修改,除非我们通过迭代器自己的删除方法,否则会抛出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());
}

Fail-Fast行为不保证在所有场景都发生,因为并发修改情况下行为不可预测。因此,基于该异常编程是不可取的。

HashSet如何保证唯一性

当我们往HashSet中增加对象元素时,对象的hashcode值决定元素是否已经存在。
对象hashcode每次计算都一样,每个hashcode值对应容纳不同对象桶的位置。但如果两个对象hashcode有可能相同,在同一桶中的对象使用equals方法进行比较。

HashSet的性能

HashSet性能受两个参数影响:初始化容量及负载因子(客座率)。

将元素添加到HashSet的预期时间复杂度是O(1),它可以在最坏的情况下(只有一个bucket)下降到O(n)——因此,保持合适的HashSet容量非常重要。

特别注意的是:从JDK8之后,最坏情况时间复杂度为O(logn)。

负载因子描述最大填充级别,超过Set则需重新扩展至原来两倍。我们使用自定义的参数可以创建HashSet:

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

第一行使用缺省值,初始容量为16,负载因子为0.75。第二行代码覆盖了缺省容量,第三行代码两个参数都被覆盖。

低初始容量节约空间,但增加重新哈希的频率,需要昂贵的开销。另一方面,高初始容量,增加迭代次数和初始内存消耗。

一般来说:

  • 高初始容量对拥有大量元素且几乎没有迭代情况有益。
  • 低初始容量对拥有较小元素且有很多迭代情况有益。

因此,在两者之间找到适当的平衡非常重要。通常情况下,默认参数经过优化且工作良好。如果我们觉得需要调整这些参数以适应需求,那么我们需要明智地进行决策选择。

总结

在本文中,我们概括了HashSet的用法及底层工作原理。了解其应用的效率及较好的性能,以及如何避免重复元素。
同时学习了HashSet的一些重要方法,能够帮助开发者充分利用其潜能。

猜你喜欢

转载自blog.csdn.net/neweastsun/article/details/80384016