【Java必修課】ArrayList與HashSet的contains方法性能比較(JMH性能測試)

1 簡介

在平常開發中,ArrayListHashSet都是Java中很經常使用的集合類。java

  • ArrayListList接口最經常使用的實現類;
  • HashSet則是保存惟一元素Set的實現。

本文主要對二者共有的方法contains()作一個簡單的討論,主要是性能上的對比,並用JMH(ava Microbenchmark Harness)進行測試比較。node

2 先看JMH測試結果

咱們使用一個由OpenJDK/Oracle裏面開發了Java編譯器的大牛們所開發的Micro Benchmark Framework來測試。下面簡單展現一下使用過程。數組

2.1 Maven導入相關依賴

導入JMH的相關依賴,能夠去官網查看最新版本:bash

<dependencies>
  <dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>${openjdk.jmh.version}</version>
  </dependency>
  <dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>${openjdk.jmh.version}</version>
  </dependency>
</dependencies>

<properties>
  <openjdk.jmh.version>1.19</openjdk.jmh.version>
</properties>

2.2 建立測試相關的類

2.2.1 集合儲存對象的類

由於要測試集合類的方法,因此咱們建立一個類來表示集合所儲存的對象。以下:源碼分析

@Data
@AllArgsConstructor(staticName = "of")
public class Student {
    private Long id;
    private String name;
}

2.2.2 JMH測試類

接下來咱們就來寫測試性能對比的類,代碼以下:性能

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ContainsPerformanceTest {
    @State(Scope.Thread)
    public static class MyState {
        private Set<Student> studentSet = new HashSet<>();
        private List<Student> studentList = new ArrayList<>();
        private Student targetStudent = Student.of(99L, "Larry");

        @Setup(Level.Trial)
        public void prepare() {
            long MAX_COUNT = 10000;
            for (long i = 0; i < MAX_COUNT; i++) {
                studentSet.add(Student.of(i, "MQ"));
                studentList.add(Student.of(i, "MQ"));
            }
            studentList.add(targetStudent);
            studentSet.add(targetStudent);
        }
    }

    @Benchmark
    public boolean arrayList(MyState state) {
        return state.studentList.contains(state.targetStudent);
    }

    @Benchmark
    public boolean hashSet(MyState state) {
        return state.studentSet.contains(state.targetStudent);
    }

    public static void main(String[] args) throws Exception {
        Options options = new OptionsBuilder()
                .include(ContainsPerformanceTest.class.getSimpleName())
                .threads(6)
                .forks(1)
                .warmupIterations(3)
                .measurementIterations(6)
                .shouldFailOnError(true)
                .shouldDoGC(true)
                .build();
        new Runner(options).run();
    }
}

測試類註解說明:測試

  • @BenchmarkMode:表示進行Benchmark時使用的模式;AverageTime表示測試調用的平均時間。
  • @OutputTimeUnit:測試的度量時間單位;NANOSECONDS表示使用納秒爲單位。
  • @State:接受一個Scope參數表示狀態的共享範圍;Scope.Thread表示每一個線程獨享。
  • @Setup:執行Benchmark前執行,相似於JUnit@BeforeAll
  • @Benchmark:進行Benchmark的對象,相似於JUnit@Test

測試類啓動參數Options說明:ui

  • include:benchmark所在的類名;
  • threads:每一個進程中的測試線程數;
  • fork:進程數,若是爲3,則JMH會fork出3個進程來測試;
  • warmupIterations:預熱的迭代次數,
  • measurementIterations:實際測量的迭代次數。

2.3 測試結果

設置好參數後,就能夠跑測試了。測試結果以下:線程

# Benchmark: ContainsPerformanceTest.arrayList

# Run progress: 0.00% complete, ETA 00:00:18
# Fork: 1 of 1
# Warmup Iteration   1: 42530.408 ±(99.9%) 2723.999 ns/op
# Warmup Iteration   2: 17841.988 ±(99.9%) 1882.026 ns/op
# Warmup Iteration   3: 18561.513 ±(99.9%) 2021.506 ns/op
Iteration   1: 18499.568 ±(99.9%) 2126.172 ns/op
Iteration   2: 18975.407 ±(99.9%) 2004.509 ns/op
Iteration   3: 19386.851 ±(99.9%) 2248.536 ns/op
Iteration   4: 19279.722 ±(99.9%) 2102.846 ns/op
Iteration   5: 19796.495 ±(99.9%) 1974.987 ns/op
Iteration   6: 21363.962 ±(99.9%) 2175.961 ns/op


Result "ContainsPerformanceTest.arrayList":
  19550.334 ±(99.9%) 2771.595 ns/op [Average]
  (min, avg, max) = (18499.568, 19550.334, 21363.962), stdev = 988.377
  CI (99.9%): [16778.739, 22321.929] (assumes normal distribution)


# Benchmark: ContainsPerformanceTest.hashSet

# Run progress: 50.00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration   1: 10.662 ±(99.9%) 0.209 ns/op
# Warmup Iteration   2: 11.177 ±(99.9%) 1.077 ns/op
# Warmup Iteration   3: 9.467 ±(99.9%) 1.462 ns/op
Iteration   1: 9.540 ±(99.9%) 0.535 ns/op
Iteration   2: 9.388 ±(99.9%) 0.365 ns/op
Iteration   3: 10.604 ±(99.9%) 1.008 ns/op
Iteration   4: 9.361 ±(99.9%) 0.154 ns/op
Iteration   5: 9.366 ±(99.9%) 0.458 ns/op
Iteration   6: 9.274 ±(99.9%) 0.237 ns/op


Result "ContainsPerformanceTest.hashSet":
  9.589 ±(99.9%) 1.415 ns/op [Average]
  (min, avg, max) = (9.274, 9.589, 10.604), stdev = 0.505
  CI (99.9%): [8.174, 11.004] (assumes normal distribution)


# Run complete. Total time: 00:00:32

Benchmark                          Mode  Cnt      Score      Error  Units
ContainsPerformanceTest.arrayList  avgt    6  19550.334 ± 2771.595  ns/op
ContainsPerformanceTest.hashSet    avgt    6      9.589 ±    1.415  ns/op

通過測試,發現二者耗時差別極大,ArrayList大概是20K納秒,而HashSet則10納秒左右。二者徹底不在一個數量級上。code

3 源碼分析

經過測試得知二者差別極大,就小窺一下源碼分析分析。

3.1 ArrayList的contains()

ArrayList的底層使用數組做爲數據存儲,當給定一個Object去判斷是否存在,須要去遍歷數組,與每一個元素對比。

public boolean contains(Object o) {
  return indexOf(o) >= 0;
}
public int indexOf(Object o) {
  if (o == null) {
    for (int i = 0; i < size; i++)
      if (elementData[i]==null)
        return i;
  } else {
    for (int i = 0; i < size; i++)
      if (o.equals(elementData[i]))
        return i;
  }
  return -1;
}

從源碼能夠發現,contains()方法是經過調用indexOf()來判斷的,然後者就是須要遍歷數組,直到找到那個與入參相等的元素纔會中止。由於,ArrayListcontains()方法的時間複雜度爲O(n),也就是說,時間取決於長度,並且是正比的關係。

3.2 HashSet的contains()

HashSet底層是經過HashMap來實現的,而HashMap的底層結構爲數組+鏈表JDK 8後改成數組+鏈表+紅黑樹

HashMap的相關代碼以下:

public boolean containsKey(Object key) {
  return getNode(hash(key), key) != null;
}
final Node<K,V> getNode(int hash, Object key) {
  Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  if ((tab = table) != null && (n = tab.length) > 0 &&
      (first = tab[(n - 1) & hash]) != null) {
    if (first.hash == hash && // always check first node
        ((k = first.key) == key || (key != null && key.equals(k))))
      return first;
    if ((e = first.next) != null) {
      if (first instanceof TreeNode)
        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
      do {
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          return e;
      } while ((e = e.next) != null);
    }
  }
  return null;
}

首先經過獲取Hash值來找,若是Hash值相等且對象也相等,則找到。通常來講,在hashCode()方法實現沒問題的狀況下,發生Hash衝突的狀況是比較少。因此能夠認爲,大部分狀況下,contains()的時間複雜度爲O(1),元素個數不影響其速度。若是發生Hash衝突,在鏈表長度小於8時,時間複雜度爲O(n);在鏈表大於8時,轉化爲紅黑樹,時間複雜度爲O(logn)

通常地,咱們認爲,HashSet/HashMap的查找的時間複雜度爲O(1)

4 總結

經過JMH測試咱們發現ArrayListHashSetcontains()方法性能差別很大。通過源碼分析得知,ArrayList對應的時間複雜度爲O(n),而HashSet的時間度爲O(1)


歡迎關注公衆號<南瓜慢說>,將持續爲你更新...

相關文章
相關標籤/搜索