java中的不可变类(Immutable)

前言

在多线程并发编程中,为了保证对象状态的原子性、可见性和有序性,开发者通常会使用 synchronized 等同步机制。

然而,过多的同步不仅会增加代码复杂度,还会影响性能。

如果我们将类设计为不可变的,那么对象的状态自始至终都不会改变。

这样一来,每次需要修改状态时都会产生一个全新的对象供不同线程使用,从而从根本上消除了并发安全问题。

一、什么是不可变类

一个类的对象在通过构造方法创建后,如果其状态绝对不会再被改变,那么这个类就是不可变(Immutable)类。

不可变对象具有以下核心特征:

  • 所有成员变量的赋值操作仅在构造方法中完成。
  • 不对外提供任何 setter 方法,彻底切断外部类修改其内部状态的途径。

二、常见的不可变类

提到不可变类,Java中最经典的就是 String 类,此外还包括 IntegerLong 等基本数据类型的包装类。String 类被设计为不可变的,主要基于以下三个方面的考量:

  1. 常量池的需要

    字符串常量池是Java堆内存中的一个特殊存储区域。创建 String 对象时,如果该字符串在常量池中不存在,则新建一个;如果已存在,则直接复用已有的引用地址。这种机制能够大幅减少JVM的内存开销,提升运行效率。

  2. hashCode 缓存

    由于字符串状态不可变,在其创建之初 hashCode 就可以被计算并缓存起来。这使得 String 非常适合作为哈希表(如 HashMap)的键,多次调用只需返回同一个哈希值,极大地提高了检索效率。

  3. 线程安全

    不可变对象天然具备线程安全性。在多线程环境下共享 String 对象时,不会出现不可预期的结果,因此也无需进行额外的同步处理。即使我们调用 String 类的 trim()substring()toLowerCase() 等方法,原字符串对象依然完好无损,方法会返回一个经过处理的全新对象。

image-20260423224716740

三、代码实践

理解不可变类很容易,但手写一个严谨的自定义不可变类需要严格遵守以下四个条件:

  1. 确保类是 final 的:防止该类被其他类继承,从而避免子类重写方法破坏其不可变性。
  2. 确保所有的成员变量是 final 的:强制字段只能在构造方法中初始化,后续绝无法被修改。
  3. 不提供任何 setter 方法:关闭外部修改对象状态的入口。
  4. 如果包含可变对象,必须返回副本:这是最容易忽略的一点。如果不可变类内部持有其他可变类的对象(例如一个普通的 Book 类),在对外提供该对象的 getter 方法时,必须返回该对象的防御性副本,绝不能直接暴露其底层的引用地址。否则,外部代码可以通过该引用地址轻易篡改原对象的数据。

代码实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 一个普通的可变类
public class Book {
private String name;
private int price;

public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getPrice() { return price; }
public void setPrice(int price) { this.price = price; }
}

// 自定义的不可变类
public final class Writer {
private final String name;
private final int age;
private final Book book;

public Writer(String name, int age, Book book) {
this.name = name;
this.age = age;
this.book = book;
}

public String getName() { return name; }
public int getAge() { return age; }

// 核心点:必须返回内部可变对象的克隆副本,防止引用地址外泄
public Book getBook() {
Book clone = new Book();
clone.setName(this.book.getName());
clone.setPrice(this.book.getPrice());
return clone;
}
}

在上面的测试用例中,如果不采用防御性拷贝返回 clone 对象,外部对返回的 Book 进行 setPrice 操作时,就会破坏 Writer 类的不可变性。

image-20260423224730847

总结

不可变类通过牺牲极少量的内存(修改时需创建新对象),换取了多线程环境下的绝对安全。

在实际开发中,尤其是构建多线程高并发系统时,这种“无锁化”的设计思路极为重要