天天看點

深入了解java之HashSet深入了解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(, 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(, 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<>();
Set<String> hashset = new HashSet<>(, f);
           

第一行使用預設值,初始容量為16,負載因子為0.75。第二行代碼覆寫了預設容量,第三行代碼兩個參數都被覆寫。

低初始容量節約空間,但增加重新哈希的頻率,需要昂貴的開銷。另一方面,高初始容量,增加疊代次數和初始記憶體消耗。

一般來說:

  • 高初始容量對擁有大量元素且幾乎沒有疊代情況有益。
  • 低初始容量對擁有較小元素且有很多疊代情況有益。

是以,在兩者之間找到适當的平衡非常重要。通常情況下,預設參數經過優化且工作良好。如果我們覺得需要調整這些參數以适應需求,那麼我們需要明智地進行決策選擇。

總結

在本文中,我們概括了HashSet的用法及底層工作原理。了解其應用的效率及較好的性能,以及如何避免重複元素。

同時學習了HashSet的一些重要方法,能夠幫助開發者充分利用其潛能。

繼續閱讀