不可变对象指的是对象一旦被创建,其内部的状态(也就是成员变量的值)就无法被修改,这种特性让它在Java的安全保障体系中占据重要地位,既能避免多线程场景下的数据错乱,也能减少很多不必要的状态校验逻辑。

什么是不可变对象
判断一个Java对象是否为不可变对象,需要满足几个核心条件:首先类不能被继承,避免子类修改其行为;其次所有的成员变量都要是私有的,并且用final关键字修饰,保证变量只能被赋值一次;最后如果成员变量是引用类型,那么这个引用指向的对象也必须是不可变的,或者类不提供修改该引用指向对象的方法。
不可变对象保障安全性的核心原理
1. 天然支持线程安全
多线程场景下出现线程安全问题的核心原因是多个线程同时修改共享对象的状态,而不可变对象的状态创建后就无法修改,所以多个线程同时访问同一个不可变对象时,不需要加锁也不需要额外的同步操作,不会出现数据不一致的问题。
我们可以通过一个简单的示例对比可变对象和不可变对象在多线程下的表现,首先看可变对象的代码:
// 可变对象示例
class MutableObject {
private int value;
public MutableObject(int value) {
this.value = value;
}
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
// 多线程修改可变对象,可能出现数据不一致
public class TestMutable {
public static void main(String[] args) throws InterruptedException {
MutableObject obj = new MutableObject(0);
// 启动10个线程,每个线程对value加1000次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
obj.setValue(obj.getValue() + 1);
}
}).start();
}
Thread.sleep(2000);
// 预期结果是10000,实际可能小于10000
System.out.println(obj.getValue());
}
}
再看不可变对象的实现,因为状态无法修改,所以多线程访问不会有问题:
// 不可变对象示例
final class ImmutableObject {
private final int value;
public ImmutableObject(int value) {
this.value = value;
}
// 只提供获取方法,不提供修改方法
public int getValue() {
return value;
}
// 如果需要修改状态,返回新的对象
public ImmutableObject add(int addValue) {
return new ImmutableObject(this.value + addValue);
}
}
// 多线程访问不可变对象,无需同步
public class TestImmutable {
public static void main(String[] args) throws InterruptedException {
ImmutableObject obj = new ImmutableObject(0);
// 启动10个线程,每个线程基于原对象创建新的加1000次的对象
ImmutableObject[] result = new ImmutableObject[10];
for (int i = 0; i < 10; i++) {
final int index = i;
new Thread(() -> {
ImmutableObject temp = obj;
for (int j = 0; j < 1000; j++) {
temp = temp.add(1);
}
result[index] = temp;
}).start();
}
Thread.sleep(2000);
// 每个线程的结果都是1000,不会出现数据错乱
for (ImmutableObject item : result) {
System.out.println(item.getValue());
}
}
}
2. 避免引用被意外修改
不可变对象的成员变量如果是引用类型,也会通过设计避免引用指向的对象被修改。比如Java中的String类,它的内部字符数组是private final的,而且String类没有提供修改这个数组的方法,即使获取到数组的引用,也无法修改其内容,这就避免了对象内部状态被意外篡改。
我们可以看String类的简化实现逻辑:
public final class String {
// 字符数组用final修饰,且是私有的
private final char value[];
public String(char value[]) {
// 复制传入的数组,避免外部修改影响内部状态
this.value = Arrays.copyOf(value, value.length);
}
// 获取字符数组时返回副本,避免外部修改内部数组
public char[] toCharArray() {
return Arrays.copyOf(value, value.length);
}
}
3. 哈希值稳定,适合作为哈希容器的键
不可变对象的状态不会变化,所以它的hashCode值也是固定的,这让它非常适合作为HashMap、HashSet等哈希容器的键。如果作为键的对象是可变的,一旦对象状态修改导致hashCode变化,就会出现容器找不到对应键的问题,而不可变对象完全避免了这种情况。
自定义不可变对象的设计要点
如果需要在项目中自定义不可变对象,需要遵循以下几个设计规则:
- 使用
final修饰类,避免被继承后修改行为 - 所有成员变量都用
private final修饰,保证只能被赋值一次 - 不提供任何修改成员变量的方法,只提供获取方法
- 如果成员变量是引用类型,构造时复制传入的引用对象,获取时返回引用对象的副本,避免外部修改内部状态
- 如果需要修改对象状态,返回一个新的不可变对象,而不是修改原对象
典型不可变类的实际应用
Java类库中有很多典型的不可变类,除了String之外,还有Integer、Long等包装类,以及BigDecimal、LocalDate等日期时间类。这些类都遵循了不可变对象的设计规则,在多线程场景和哈希容器使用中都能保障安全性,开发者可以优先选择这些类来完成相关功能开发。