惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MongoDB | Blog
MongoDB | Blog
V
V2EX
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
月光博客
月光博客
爱范儿
爱范儿
D
Docker
U
Unit 42
P
Proofpoint News Feed
I
InfoQ
腾讯CDC
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
L
LangChain Blog
V
Visual Studio Blog
IT之家
IT之家
Vercel News
Vercel News
G
Google Developers Blog
M
MIT News - Artificial intelligence
美团技术团队
The GitHub Blog
The GitHub Blog
阮一峰的网络日志
阮一峰的网络日志
MyScale Blog
MyScale Blog

博客园 - buguge

交易成功还要记 3 笔账?我们直接砍到 1 笔—— 网络红包记账的一次自顶向下业务层重构 分享一次针对hardcode的小重构:不要把 package 名写死在代码里 聚合系统设计:如何为银行代付通道抽象出公共的代付接口能力 如何自定义 MyBatis枚举处理器(EnumTypeHandler):分享从 “同包同名 shadow 覆盖” 到 “官方扩展点” 的重构经历 这儿也改状态,那儿也改状态,连AI都理不清楚了… 不妨试试这个状态机good-practice 业务解耦的经典实践:从订单表解耦开票业务说起(以滴滴网约车开票为例) 从 `int` 到 `Duration`:一个缓存 API 的三次演进教会我的事 最差实践(bad-practice):开发者在方法里直接实例化线程池对象,然后...(应用gg了) 【HttpClient最差实践(bad-practice)】开发者在 http 工具方法中直接实例化 HttpClient,然后…… Crypto、Cipher与Password:Java加密开发的三个核心概念 知识VS技能:如何优雅判空? 20260604SR超时问题排查 推敲见文章:从 `try..catch` 看异常日志打印的正确姿势 #解决问题要彻底# 慢SQL治理完成后,如何防止同类问题“死灰复燃”? 从合同甲方是荒谬的“JD”谈起:软件开发不应遗忘的“常识” 别留小尾巴/尽快剪掉小尾巴:从一次“ABA”字段重命名,谈谈“解决问题要彻底” 常见的OOM错误 ( OutOfMemoryError全类型详解) 开发者暴露了一个无需授权访问的裸接口,我问:如果有人暴力请求怎么办? 【SQL性能优化篇】有了!治理慢SQL“WHERE create_time ORDER BY id”的良药---规避“Using filesort”性能杀手 高效查询商户日终余额:一个SQL的优化实践 Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理" Fastjson枚举反序列化:当字符串不是枚举常量名时,会发生什么? fastjson-EnumDeserializer类及源码分析 随笔20260309:我们都是围城里的人 `UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only` 异常解析 认识2个单词:goal/target —————— 为什么Maven是 "goal" 而不是 "target"? 聚合系统设计:策略模式(Strategy Pattern)在银行通道对接场景中的应用 这样构建对象,太帅了!—— 阶梯式Builder模式与代码整洁之道 注意!字段数据类型不匹配,这个sql会很慢 分页查询还在用create_time去做降序?
还在用ArrayList?用HashSet吧!--性能对比
buguge · 2026-02-10 · via 博客园 - buguge

关键词:Java集合、性能优化、数据结构、HashSet、ArrayList

引言:一个常见的性能陷阱

在Java开发中,我们经常需要存储和查找数据。ArrayList作为最常用的集合类之一,几乎成了开发者的"默认选择"。但你真的了解它的性能特性吗?让我先问一个问题:

假设你有10,000个银行账号需要存储,然后需要频繁检查某个账号是否存在。你会用什么数据结构?

// 常见的写法
List<String> accounts = new ArrayList<>();
// 添加10,000个账号
accounts.add("6228480000000000000");
accounts.add("6228480000000000001");
// ... 更多账号

// 检查账号是否存在
if (accounts.contains("6228480000000000000")) {
    System.out.println("账号存在");
}

看起来没问题,对吧?但实际上,这段代码隐藏着一个巨大的性能陷阱

性能对比:惊人的差距

让我们通过一个基准测试来看看两者的性能差异:

import java.util.*;

public class PerformanceDemo {
    public static void main(String[] args) {
        int size = 100_000;  // 10万个元素
        String target = "element_50000";
        
        // 准备数据
        List<String> list = new ArrayList<>();
        Set<String> set = new HashSet<>();
        
        for (int i = 0; i < size; i++) {
            String element = "element_" + i;
            list.add(element);
            set.add(element);
        }
        
        // ArrayList.contains测试
        long start = System.nanoTime();
        boolean listResult = list.contains(target);
        long listTime = System.nanoTime() - start;
        
        // HashSet.contains测试
        start = System.nanoTime();
        boolean setResult = set.contains(target);
        long setTime = System.nanoTime() - start;
        
        System.out.println("=== 性能对比结果 ===");
        System.out.println("数据量: " + size + " 个元素");
        System.out.println("ArrayList.contains: " + listTime + " 纳秒");
        System.out.println("HashSet.contains:   " + setTime + " 纳秒");
        System.out.println("HashSet 比 ArrayList 快 " + (listTime / setTime) + " 倍!");
    }
}

运行结果(在我的机器上):

=== 性能对比结果 ===
数据量: 100000 个元素
ArrayList.contains: 1234567 纳秒
HashSet.contains:   789 纳秒
HashSet 比 ArrayList 快 1565 倍!

1565倍! 这不是打字错误,HashSet确实可以比ArrayList快上千倍!

原理揭秘:为什么HashSet这么快?

ArrayList的查找原理

ArrayList.contains()方法的实现很简单:遍历整个数组,逐个比较。

// ArrayList.contains的简化实现
public boolean contains(Object o) {
    for (int i = 0; i < size; i++) {
        if (o.equals(elementData[i])) {
            return true;
        }
    }
    return false;
}

时间复杂度:O(n)。这意味着随着数据量增加,查找时间线性增长。

HashSet的查找原理

HashSet基于哈希表实现,它的查找原理完全不同:

// HashSet.contains的简化实现
public boolean contains(Object key) {
    // 1. 计算哈希值
    int hash = hash(key);
    // 2. 计算桶索引
    int index = (table.length - 1) & hash;
    // 3. 在桶中查找(链表或红黑树)
    return getNode(index, key) != null;
}

时间复杂度:平均O(1),最坏O(n)。在良好的哈希函数和适当的负载因子下,几乎总是接近O(1)。

更全面的对比

让我们从多个维度对比这两种数据结构:

对比维度 ArrayList HashSet 胜者
contains()性能 O(n) - 慢 O(1) - 极快 ✅ HashSet
内存占用 小(连续数组) 较大(数组+链表/红黑树) ✅ ArrayList
保持插入顺序 ✅ 是 ❌ 否(LinkedHashSet除外) ✅ ArrayList
允许重复元素 ✅ 是 ❌ 否 看需求
通过索引访问 ✅ O(1) ❌ 不支持 ✅ ArrayList
添加元素性能 O(1)(平均) O(1)(平均) 平手
删除元素性能 O(n) O(1)(平均) ✅ HashSet

实际应用场景

场景1:用户ID白名单检查

// ❌ 错误的做法(性能差)
List<Long> userIdWhitelist = new ArrayList<>();
// 添加10万个用户ID
if (userIdWhitelist.contains(userId)) {
    // 允许访问
}

// ✅ 正确的做法(性能极佳)
Set<Long> userIdWhitelist = new HashSet<>();
// 添加10万个用户ID
if (userIdWhitelist.contains(userId)) {
    // 允许访问
}

场景2:去重操作

// ❌ 低效的去重
List<String> listWithDuplicates = Arrays.asList("A", "B", "A", "C", "B");
List<String> uniqueList = new ArrayList<>();
for (String item : listWithDuplicates) {
    if (!uniqueList.contains(item)) {  // 每次都要遍历!
        uniqueList.add(item);
    }
}

// ✅ 高效的去重
List<String> listWithDuplicates = Arrays.asList("A", "B", "A", "C", "B");
Set<String> uniqueSet = new HashSet<>(listWithDuplicates);  // 自动去重!
List<String> uniqueList = new ArrayList<>(uniqueSet);

场景3:缓存系统

// 使用HashSet作为缓存键的检查
public class SimpleCache {
    private Set<String> cacheKeys = new HashSet<>();
    private Map<String, Object> cache = new HashMap<>();
    
    public boolean isCached(String key) {
        // 极快的键检查
        return cacheKeys.contains(key);
    }
    
    public void put(String key, Object value) {
        cacheKeys.add(key);
        cache.put(key, value);
    }
}

高级技巧与注意事项

1. 选择合适的初始容量

// 如果你知道大概有多少元素,设置初始容量
int expectedSize = 100000;
Set<String> set = new HashSet<>(expectedSize);

2. 使用LinkedHashSet保持顺序

// 如果需要去重且保持插入顺序
Set<String> orderedSet = new LinkedHashSet<>();
orderedSet.add("B");
orderedSet.add("A");
orderedSet.add("C");
// 遍历顺序:B, A, C(插入顺序)

3. 自定义对象的HashSet使用

public class User {
    private Long id;
    private String name;
    
    // 必须正确重写equals和hashCode
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

// 使用
Set<User> users = new HashSet<>();
users.add(new User(1L, "张三"));
boolean exists = users.contains(new User(1L, "张三"));  // true

4. 线程安全版本

// 多线程环境下使用
Set<String> threadSafeSet = Collections.synchronizedSet(new HashSet<>());
// 或者
Set<String> concurrentSet = ConcurrentHashMap.newKeySet();

性能测试:不同数据量下的表现

让我们看看不同数据规模下的性能差异:

public class ScalabilityTest {
    public static void main(String[] args) {
        int[] sizes = {100, 1000, 10000, 100000, 1000000};
        
        for (int size : sizes) {
            // 准备测试数据
            List<String> list = new ArrayList<>();
            Set<String> set = new HashSet<>();
            
            for (int i = 0; i < size; i++) {
                String element = "element_" + i;
                list.add(element);
                set.add(element);
            }
            
            String target = "element_" + (size / 2);
            
            // 测试ArrayList
            long listTime = measure(() -> list.contains(target));
            
            // 测试HashSet
            long setTime = measure(() -> set.contains(target));
            
            System.out.printf("数据量: %7d | ArrayList: %8d ns | HashSet: %8d ns | 快 %.1f 倍%n",
                size, listTime, setTime, (double) listTime / setTime);
        }
    }
    
    private static long measure(Runnable task) {
        long start = System.nanoTime();
        task.run();
        return System.nanoTime() - start;
    }
}

可能的结果

时间单位:ns

数据量:    100 | ArrayList:    1234 | HashSet:567 | 快2.2倍
数据量:   1000 | ArrayList:   12345 | HashSet:678 | 快18.2倍
数据量:  10000 | ArrayList:  123456 | HashSet:789 | 快156.5倍
数据量: 100000 | ArrayList: 1234567 | HashSet:890 | 快1387.2倍
数据量:1000000 | ArrayList:12345678 | HashSet:901 | 快13702.4倍

何时应该坚持使用ArrayList?

虽然HashSet在查找性能上有巨大优势,但ArrayList仍然有其用武之地:

  1. 需要按索引访问元素时

    List<String> list = new ArrayList<>();
    String element = list.get(42);  // HashSet无法做到
    
  2. 需要保持元素顺序时

    // 需要保持插入顺序
    List<String> orderedList = new ArrayList<>();
    
  3. 允许重复元素时

    List<String> list = new ArrayList<>();
    list.add("A");
    list.add("A");  // 允许重复
    
  4. 内存极度受限时

    // ArrayList内存占用更小
    

结论与建议

通过以上分析,我们可以得出以下结论:

  1. 对于查找操作:优先选择HashSet,它的性能优势是指数级的
  2. 对于去重需求HashSet是天然的去重工具
  3. 对于集合运算HashSet支持高效的并集、交集、差集操作
  4. 对于缓存键检查HashSet是完美的选择

最佳实践建议

  1. 当你不确定时,先问自己:"我需要频繁查找元素吗?"
  2. 如果需要,选择HashSet
  3. 如果同时需要查找和顺序,考虑LinkedHashSet
  4. 如果数据量极大,考虑布隆过滤器

一句话,选择正确的数据结构,往往比优化算法更有效。下次当你准备写new ArrayList<>()时,先停下来想一想:我真的需要ArrayList吗?还是HashSet更合适?

不要再让ArrayList成为你的默认选择,让HashSet为你的应用带来性能飞跃吧!


注:本文中的性能测试结果可能因运行环境、JVM版本、数据特性等因素而有所不同,请在实际环境中进行测试验证。