1. 概述

本文将深入讲解什么是不可变对象,如何在 Java 中实现不可变性,以及这样做的优势。内容简洁实用,适合有一定经验的开发者快速掌握核心要点,避免踩坑。

2. 什么是不可变对象?

一个不可变对象指的是:✅ 对象一旦创建完成,其内部状态就永远不会改变

这意味着该对象的公共 API 能够保证它在整个生命周期中行为一致,不会出现“中途变卦”的情况。

String 类为例,虽然它的 replace 方法看起来像是在修改字符串,但实际上原始对象并未被改动:

String name = "baeldung";
String newName = name.replace("dung", "----");

assertEquals("baeldung", name);
assertEquals("bael----", newName);

⚠️ 关键点:String 的所有“修改”方法其实都返回一个新实例,原对象保持不变。这也是它被设计为不可变类的典型体现。

不可变对象的 API 应该只提供只读方法,❌ 绝不应该包含任何会修改内部状态的操作。

3. Java 中的 final 关键字

在实现不可变性之前,必须搞清楚 final 的真正含义。

Java 中变量默认是可变的 —— 你可以随时更改它所持有的值。但一旦加上 final,编译器就会阻止你再次赋值:

final String name = "baeldung";
name = "bael..."; // ❌ 编译错误!

⚠️ 重要提醒:final 只保证引用不被重新赋值,并不能防止通过该引用去修改对象内部状态。例如下面这个常见误区:

final List<String> strings = new ArrayList<>();
assertEquals(0, strings.size());
strings.add("baeldung");
assertEquals(1, strings.size()); // ✅ 成功!说明 List 内容变了

尽管 stringsfinal 的,但我们依然可以通过 .add() 修改其内容。因此,ArrayList 本身并不是不可变集合。

4. 如何在 Java 中实现不可变对象

要真正构建一个不可变类,仅靠 final 字段还不够,需要从设计层面严格把控。以下是关键步骤:

✅ 核心原则

  1. 所有字段用 private final 修饰
  2. 不提供任何 setter 或修改状态的方法
  3. 所有方法只能返回数据副本或不变视图
  4. 构造函数中完成状态初始化,且避免泄露 this 引用
  5. 如果字段是可变类型(如集合、日期等),必须防御性拷贝

示例:实现一个不可变的 Money 类

class Money {
    private final double amount;
    private final Currency currency;

    public Money(double amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public Currency getCurrency() {
        return currency;
    }

    public double getAmount() {
        return amount;
    }

    // 提供操作方法时,返回新对象而不是修改自身
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currencies must match");
        }
        return new Money(this.amount + other.amount, this.currency);
    }

    @Override
    public boolean equals(Object o) { /* 省略 equals 实现 */ }
    
    @Override
    public int hashCode() { /* 省略 hashCode 实现 */ }
}

⚠️ 踩坑警告:防御性拷贝

如果类中包含可变对象(比如 DateCollection),必须做深拷贝,否则外部仍可能篡改内部状态。

举个反例:

public class Event {
    private final Date when;

    public Event(Date when) {
        this.when = when; // ❌ 危险!外部还能修改 when
    }

    public Date getWhen() {
        return when;
    }
}

正确做法是:

public Event(Date when) {
    this.when = new Date(when.getTime()); // ✅ 防御性拷贝
}

public Date getWhen() {
    return new Date(when.getTime()); // ✅ 返回副本
}

对于集合类型也类似,建议使用 Collections.unmodifiableList 包装:

private final List<String> tags;

public Event(List<String> tags) {
    this.tags = List.copyOf(tags); // ✅ Java 10+ 推荐写法
    // 或者:new ArrayList<>(tags),再用 Collections.unmodifiableList 包装
}

public List<String> getTags() {
    return Collections.unmodifiableList(tags); // ✅ 返回不可变视图
}

5. 不可变对象的优势

不可变对象不是为了炫技,而是解决实际问题的利器。主要好处包括:

  • 线程安全:无需同步控制,可被多个线程并发访问而无需担心状态不一致
  • 避免副作用:函数调用不会意外改变传入的对象,适合函数式编程风格
  • 缓存友好hashCodeequals 结果稳定,适合用作 Map 的 key 或 Set 元素
  • 简化调试:对象状态固定,出问题更容易追踪
  • 天然防御:防止调用方修改内部数据,提升封装性

特别是在高并发场景下,不可变对象能大幅降低锁竞争,提升系统吞吐量。

6. 总结

不可变对象的核心思想很简单:✅ 创建即定型,状态永不变更

通过合理使用 final、防御性拷贝和只读 API,我们可以构建出安全、可靠、易于维护的不可变类。它们在多线程环境、缓存机制、领域模型设计中都有广泛应用。

📌 小贴士:JDK 中典型的不可变类包括 StringIntegerLocalDateTimeCollections.unmodifiableXXX 返回的对象等。设计时不妨多参考这些“教科书级”实现。

邮箱联系:dev@baeldung.com
原文来源:Baeldung - Immutable Objects in Java


原始标题:Immutable Objects in Java | Baeldung