在平常开发中,ArrayList
和HashSet
都是Java中很经常使用的集合类。java
ArrayList
是List
接口最经常使用的实现类;HashSet
则是保存惟一元素Set
的实现。本文主要对二者共有的方法contains()
作一个简单的讨论,主要是性能上的对比,并用JMH(ava Microbenchmark Harness)
进行测试比较。node
咱们使用一个由OpenJDK/Oracle里面开发了Java编译器的大牛们所开发的Micro Benchmark Framework
来测试。下面简单展现一下使用过程。数组
导入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>
由于要测试集合类的方法,因此咱们建立一个类来表示集合所储存的对象。以下:源码分析
@Data @AllArgsConstructor(staticName = "of") public class Student { private Long id; private String name; }
接下来咱们就来写测试性能对比的类,代码以下:性能
@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(); } }
测试类注解说明:测试
AverageTime
表示测试调用的平均时间。NANOSECONDS
表示使用纳秒为单位。Scope
参数表示状态的共享范围;Scope.Thread
表示每一个线程独享。JUnit
的@BeforeAll
。JUnit
的@Test
。测试类启动参数Options
说明:ui
设置好参数后,就能够跑测试了。测试结果以下:线程
# 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
经过测试得知二者差别极大,就小窥一下源码分析分析。
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),也就是说,时间取决于长度,并且是正比的关系。
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)。
经过JMH
测试咱们发现ArrayList
和HashSet
的contains()
方法性能差别很大。通过源码分析得知,ArrayList
对应的时间复杂度为O(n),而HashSet
的时间度为O(1)。
欢迎关注公众号<南瓜慢说>,将持续为你更新...