bigywater.tech
Java 内部原理 · 23

enum.values() 每次调用都真实克隆一个新数组:真实测出循环里调用慢了近 2 倍

for (Color c : Color.values()) 这种写法几乎每个 Java 项目里都能找到,但很少有人真的验证过:values() 每次调用到底是不是真的重新创建了一个数组。

a == b → false
真实验证:两次 Color.values() 调用返回的数组,用 == 比较是 false——真的是两个不同的数组实例
约 2 倍
真实计时:2000 万次循环,每次都调用 values() 比缓存后复用同一个数组慢约 2 倍,两次独立运行一致

上一篇:LongAdder 比 AtomicLong 快多少:16 线程下真实测出 40 倍以上

真实验证:每次调用真的是新数组

enum 类型的 values() 方法是编译器自动生成的,内部原理是克隆一份存着所有枚举实例的私有静态数组。

真实实测 · 两次 values() 调用对比
两次 values() 调用返回的数组 == 比较(是否同一个实例) = false
两次调用返回的数组内容 equals(Arrays.equals) = true
修改一次 values() 返回的数组后,下次 values()[0] = RED(预期还是 RED,不受影响)

三行真实结果拼出完整的行为:两次调用拿到的是不同的数组实例(== 为 false),但内容完全相同(equals 为 true)——说明每次调用都真实创建了一份新的、内容一致的克隆。第三行更直接验证了"为什么要克隆":如果不克隆、直接把内部私有数组暴露出去,调用方改一下返回值(比如 values()[0] = BLACK)就能真实污染所有枚举类的内部状态;真实测出改了这次拿到的数组之后,下次调用 values() 结果完全不受影响,说明克隆确实起到了隔离作用。

真实验证:热路径里反复调用,代价是真实的

克隆一个只有 6 个元素的小数组,单次开销很小,但如果这行代码在一个执行 2000 万次的循环里,真实差距会被放大到能测出来。

真实实测 · 2000 万次循环
每次调用 values() 真实耗时 = 159ms (sum=300000000)
缓存后复用同一个数组耗时   = 79ms (sum=300000000)
倍数 = 2.0

第二次独立运行倍数是 1.9,结果稳定在 2 倍左右。两组的求和结果完全一致(sum=300000000),证明计算内容真实等价,差距纯粹来自 values() 每次调用都要真实分配一个新数组并把内容拷贝进去这一步。

2 倍算多吗?对绝大多数场景来说,这点差距真实存在但影响很小——毕竟大多数代码里 values() 不会被调用几千万次。真正值得注意的场景是:这行代码出现在一个本身就高频执行的热路径循环内部(比如某个请求处理逻辑每次都要遍历一遍枚举做校验)。这种情况下,把 values() 的结果缓存成一个静态常量(比如 private static final Color[] VALUES = values();)是一个真实、低成本、可验证的优化,不是过早优化。

分步演示:为什么每次调用 values() 都要重新分配一个数组

每一行都是真实运行这段代码得到的结果。

Color.values() 真实每次都返回一个新数组(克隆内部私有数组),而不是共享同一个数组引用
这一步新验证的比较

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,两次独立计时倍数一致(1.9 和 2.0)。
    • "每次调用都返回新对象,是为了防止内部状态被外部篡改"这个设计思路,和Arrays.asList 那篇视图/拷贝的权衡是相关但相反的例子,建议对照读。
    • 没有涉及:EnumMap/EnumSet 内部基于 ordinal() 的位图实现——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号