
Lombok 的 @Value 注解可自动生成 final 字段、无参构造器、getter 和不可变方法签名,但仅提供浅层不可变性——它不阻止数组或可变对象内部状态被修改,需配合防御性拷贝等手段实现真正不可变。
lombok 的 @value 注解可自动生成 final 字段、无参构造器、getter 和不可变方法签名,但**仅提供浅层不可变性**——它不阻止数组或可变对象内部状态被修改,需配合防御性拷贝等手段实现真正不可变。
Lombok 的 @Value 是构建不可变值类的强力工具,但它所保障的“不可变性”是编译期与结构层面的:它会将所有字段设为 final、禁止生成 setter、并默认使用私有全参构造器。然而,这仅能防止字段引用被重新赋值(即不能 t.tags = new String[0]),无法阻止引用对象自身状态的改变——而这正是你测试中发现的问题根源。
在你的示例中:
protected String[] tags;
虽然 tags 字段是 final 的,但 String[] 是一个可变的引用类型:数组内容可被原地修改(如 tags[0] = "?"),且 getTags() 返回的是原始数组引用,调用方拿到后即可直接操作其元素。因此,即使 @Value 生成了 final 字段和只读 getter 签名,t.getTags()[0] = "?" 依然合法且生效。
✅ 正确做法:实现深层不可变性
需在两个关键位置进行防御性拷贝(Defensive Copy):
- 构造时拷贝入参(防止外部传入的数组被后续修改影响内部状态)
- getter 中返回新拷贝(防止内部数组被外部修改)
以下是修正后的 ValueExample 实现(手动补充防御逻辑,因 Lombok 不自动处理数组):
import lombok.Value;
import java.util.Arrays;
@Value
public class ValueExample {
String name;
int age; // 移除 @NonFinal —— @Value 默认所有字段 final
double score;
String[] tags; // 保持字段名一致,但需手动管控
// 自定义构造器,对 tags 进行深拷贝
public ValueExample(String name, int age, double score, String[] tags) {
this.name = name;
this.age = age;
this.score = score;
this.tags = tags == null ? new String[0] : Arrays.copyOf(tags, tags.length);
}
// 重写 getter,返回新拷贝(避免暴露内部数组)
public String[] getTags() {
return tags == null ? null : Arrays.copyOf(tags, tags.length);
}
}
⚠️ 注意事项:
-
@Value会忽略protected/private修饰符,统一生成publicgetter,因此protected String[] tags实际无意义;应直接使用包级或private并依赖 getter 控制访问。 - 若
tags元素类型为可变对象(如List<mutablebean></mutablebean>),还需确保每个元素本身不可变,或使用不可变集合(如List.copyOf(...)或 Guava 的ImmutableList)。 - 对于集合类型,推荐优先使用
java.util.List+List.of()(Java 9+)或Collections.unmodifiableList()封装,而非原始数组。 - 性能权衡:频繁拷贝大数组会带来开销。若场景允许“不可变契约”(即约定不修改),可辅以文档说明 +
@Immutable注解(如 JSR-305)作为提示,而非强制拷贝。
总结:@Value 是不可变设计的良好起点,但真正的不可变性需要开发者对引用类型(尤其是数组、集合、自定义可变对象)主动防御。理解“浅不可变”与“深不可变”的区别,是写出健壮值对象的关键。










