1. 概述
本文将通过简单示例深入探讨Java中Set
和List
的核心区别,并从性能和内存分配角度对比两种数据结构。有经验的开发者都知道,选择合适的集合类型直接影响程序效率——用错可能就是无形的性能杀手。
2. 概念差异
List
和Set
虽同属Java集合框架,但存在关键区别:
- 重复处理:
List
允许重复元素,Set
则严格禁止 - 顺序性:
List
保持插入顺序,Set
则不一定(取决于具体实现) - 访问方式:
Set
因顺序不保证,不支持List
那样的索引访问
注意:部分
Set
实现(如LinkedHashSet
)会维护插入顺序,但这并非通用特性。
3. 代码示例
3.1. 重复元素处理
List
对重复元素来者不拒,Set
则直接拒绝:
@Test
public void givenList_whenDuplicates_thenAllowed(){
List<Integer> integerList = new ArrayList<>();
integerList.add(2);
integerList.add(3);
integerList.add(4);
integerList.add(4); // 重复添加
assertEquals(integerList.size(), 4); // 正确通过
}
@Test
public void givenSet_whenDuplicates_thenNotAllowed(){
Set<Integer> integerSet = new HashSet<>();
integerSet.add(2);
integerSet.add(3);
integerSet.add(4);
integerSet.add(4); // 重复添加被忽略
assertEquals(integerSet.size(), 3); // 正确通过
}
3.2. 插入顺序维护
Set
的顺序性取决于具体实现:HashSet
不保证顺序,LinkedHashSet
则严格维护。看个LinkedHashSet
的例子:
@Test
public void givenSet_whenOrdering_thenMayBeAllowed(){
Set<Integer> set1 = new LinkedHashSet<>();
set1.add(2);
set1.add(3);
set1.add(4);
Set<Integer> set2 = new LinkedHashSet<>();
set2.add(2);
set2.add(3);
set2.add(4);
Assert.assertArrayEquals(set1.toArray(), set2.toArray()); // 顺序一致
}
由于Set
不保证顺序,自然也就不支持索引访问——这是和List
的硬性区别。
4. List与Set性能对比
我们使用JMH进行基准测试,创建ListAndSetAddBenchmark
和ListAndSetContainBenchmark
两个测试类,分别测量add()
和contains()
操作的性能。
4.1. JMH参数配置
@BenchmarkMode(Mode.SingleShotTime)
@Warmup(iterations = 3, time = 10, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.MILLISECONDS)
public class ListAndSetAddBenchmark {
}
关键注解说明:
-
@BenchmarkMode(Mode.SingleShotTime)
:单次执行模式,测量完整操作耗时 -
@Warmup
:预热3轮,每轮10毫秒(JVM优化需要预热) -
@Measurement
:正式测量3轮,每轮10毫秒
4.2. add()操作性能
先定义测试状态类:
@State(Scope.Benchmark)
public static class Params {
public int addNumber = 10000000;
public List<Integer> arrayList = new ArrayList<>();
public Set<Integer> hashSet = new HashSet<>();
}
ArrayList
的add()
测试:
@Benchmark
public void addElementsToArrayList(Params param, Blackhole blackhole) {
param.arrayList.clear();
for (int i = 0; i < param.addNumber; i++) {
blackhole.consume(param.arrayList.add(i));
}
}
HashSet
的add()
测试:
@Benchmark
public void addElementToHashSet(Params param, Blackhole blackhole) {
param.hashSet.clear();
for (int i = 0; i < param.addNumber; i++) {
blackhole.consume(param.hashSet.add(i));
}
}
测试结果:
Benchmark Mode Cnt Score Error Units
addElementToArrayList ss 15 0.386 ± 1.266 s/op
addElementToHashSet ss 15 0.419 ± 2.535 s/op
结论:批量添加时ArrayList
更快(0.386s vs 0.419s)。当场景要求快速插入时,List
是更优选择。
4.3. contains()操作性能
定义包含大量元素的测试状态:
@State(Scope.Benchmark)
public static class Params {
@Param({"5000000"})
public int searchElement;
@Param({"10000000"})
public int collectionSize;
public List<Integer> arrayList;
public Set<Integer> hashSet;
@Setup(Level.Iteration)
public void setup() {
arrayList = new ArrayList<>();
hashSet = new HashSet<>();
for (int i = 0; i < collectionSize; i++) {
arrayList.add(i);
hashSet.add(i);
}
}
}
ArrayList
的contains()
测试:
@Benchmark
public void searchElementInArrayList(Params param, Blackhole blackhole) {
for (int i = 0; i < param.containNumber; i++) {
blackhole.consume(param.arrayList.contains(param.searchElement));
}
}
HashSet
的contains()
测试:
@Benchmark
public void searchElementInHashSet(Params param, Blackhole blackhole) {
for (int i = 0; i < param.containNumber; i++) {
blackhole.consume(param.hashSet.contains(param.searchElement));
}
}
测试结果:
Benchmark Mode Cnt Score Error Units
searchElementInArrayList ss 15 0.014 ± 0.015 s/op
searchElementInHashSet ss 15 ≈ 10⁻⁵ s/op
结论:**HashSet
的查找性能碾压ArrayList
**(10⁻⁵秒 vs 0.014秒)。高频查找场景下,Set
的优势是压倒性的。
5. 内存分配对比
通过JMH的-prof gc
选项分析内存分配:
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ListAndSetAddBenchmark.class.getSimpleName())
.forks(1)
.addProfiler("gc")
.build();
new Runner(opt).run();
}
内存分配结果:
Benchmark Mode Cnt Score Error Units
addElementToArrayList:·gc.alloc.rate ss 3 172.685 ± 254.719 MB/sec
addElementToHashSet:·gc.alloc.rate ss 3 504.746 ± 1323.322 MB/sec
searchElementInArrayList:·gc.alloc.rate ss 3 248.628 ± 395.569 MB/sec
searchElementInHashSet:·gc.alloc.rate ss 3 254.192 ± 235.294 MB/sec
关键发现:
- 添加操作:
HashSet
内存分配率高达504.746 MB/sec,远超ArrayList
的172.685 MB/sec - 查找操作:
HashSet
内存分配略高(254.192 vs 248.628 MB/sec),但差异不显著
误差值较大(±)说明测试受JVM预热、代码优化等因素影响,但趋势明显。
6. 结论
通过本文我们明确了:
- 功能差异:
List
允许重复+有序,Set
去重+无序(特定实现除外) - 性能权衡:
- 批量插入选
List
(速度更快,内存更省) - 高频查找选
Set
(性能碾压,但内存开销大)
- 批量插入选
- 内存成本:
HashSet
的哈希结构带来查找优势,但内存消耗显著高于ArrayList
实际开发中,根据操作类型选择集合类型——没有银弹,只有取舍。源码已上传至GitHub。