1. 概述

本文将通过简单示例深入探讨Java中SetList的核心区别,并从性能和内存分配角度对比两种数据结构。有经验的开发者都知道,选择合适的集合类型直接影响程序效率——用错可能就是无形的性能杀手。

2. 概念差异

ListSet虽同属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进行基准测试,创建ListAndSetAddBenchmarkListAndSetContainBenchmark两个测试类,分别测量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<>(); 
}

ArrayListadd()测试:

@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));
    }
}

HashSetadd()测试:

@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);
        }
    }
}

ArrayListcontains()测试:

@Benchmark
public void searchElementInArrayList(Params param, Blackhole blackhole) {
    for (int i = 0; i < param.containNumber; i++) {
        blackhole.consume(param.arrayList.contains(param.searchElement));
    }
}

HashSetcontains()测试:

@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


原始标题:Set vs List in Java | Baeldung