1 簡介
在日常開發中,ArrayList
和HashSet
都是Java中很常用的集合類。
ArrayList
是List
接口最常用的實現類;HashSet
則是保存唯一元素Set
的實現。
本文主要對兩者共有的方法contains()
做一個簡單的討論,主要是性能上的對比,並用JMH(ava Microbenchmark Harness)
進行測試比較。
2 先看JMH測試結果
我們使用一個由OpenJDK/Oracle裏面開發了Java編譯器的大牛們所開發的Micro Benchmark Framework
來測試。下面簡單展示一下使用過程。
2.1 Maven導入相關依賴
導入JMH
的相關依賴,可以去官網查看最新版本:
<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
說明:
- 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納秒左右。兩者完全不在一個數量級上。
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()
來判斷的,而後者就是需要遍歷數組,直到找到那個與入參相等的元素纔會停止。因爲,ArrayList
的contains()
方法的時間複雜度爲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
測試我們發現ArrayList
和HashSet
的contains()
方法性能差異很大。經過源碼分析得知,ArrayList
對應的時間複雜度爲O(n),而HashSet
的時間度爲O(1)。
歡迎關注公衆號<南瓜慢說>,將持續爲你更新...