小易说IT 小易说IT

Java 实现不可变类(Immutable Class)

不可变类:对象一旦创建完成,所有成员状态不能再修改,String、Integer、BigDecimal 都是典型代表。 核心思想:禁止外部修改 + 内部也不提供修改途径

✅ 实现 5 条核心规则(面试标准答法)

  1. 类使用 final 修饰 防止被子类继承,子类重写方法破坏不可变性。

    public final class User { ... }
    
  2. 所有成员变量私有化 private,并且 final 修饰

    • private:外部无法直接访问

    • final:赋值一次后引用不可重新指向别的对象

    ⚠️ 注意:final 只是引用不可变,如果成员是可变对象(ArrayList、Date、自定义对象),对象内部字段依然可以改!

  3. 不提供任何 setter 修改方法 没有任何方法可以修改成员。

  4. 构造器初始化所有字段,并且做【防御拷贝】(重点难点) 如果字段是可变引用类型,构造的时候不能直接赋值外部传入的引用,要拷贝一份。

  5. getter 方法返回时,也要做防御拷贝 返回可变对象时,不能直接返回原引用,防止外部拿到引用修改内部数据。

📌 完整示例代码(区分基本类型 & 可变引用类型)

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

/**
 * 不可变类示例
 */
public final class Person {
    // 基本类型 / 不可变类型:直接final即可
    private final long id;
    private final String name;

    // 可变引用类型:Date、List,必须防御拷贝
    private final Date birthDay;
    private final List<String> tags;

    // 构造器:一次性初始化全部字段
    public Person(long id, String name, Date birthDay, List<String> tags) {
        this.id = id;
        this.name = name;

        // ✅ 防御拷贝:不要直接 this.birthDay = birthDay
        this.birthDay = birthDay == null ? null : new Date(birthDay.getTime());

        // ✅ List防御拷贝 + 包装为不可变集合
        if (tags == null) {
            this.tags = Collections.emptyList();
        } else {
            this.tags = Collections.unmodifiableList(new ArrayList<>(tags));
        }
    }

    // 只有getter,没有setter
    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    // Date是可变对象,get的时候也要拷贝一份出去
    public Date getBirthDay() {
        return birthDay == null ? null : new Date(birthDay.getTime());
    }

    // 返回不可变List,外部无法add/remove
    public List<String> getTags() {
        return tags;
    }
}

错误反面示例(不做防御拷贝,不可变性被破坏)

// ❌ 危险写法
public Person(Date birthDay){
    this.birthDay = birthDay;
}
// 外部调用:
Date d = new Date();
Person p = new Person(d);
d.setTime(99999999); // 直接修改了Person内部的birthDay!

📎 补充:其他可选优化点

  1. 构造器私有 + 静态工厂方法(推荐) 可以不对外暴露构造函数,用 of() 创建实例,方便缓存、复用对象(类似 Integer.valueOf 缓存)

    public final class Person {
        private Person(...) { ... }
        public static Person of(long id, String name, Date birthDay, List<String> tags){
            return new Person(id, name, birthDay, tags);
        }
    }
    
  2. 重写 equals()、hashCode()、toString() 不可变对象非常适合作为 HashMap key,一定要正确重写。

  3. 序列化:如果实现 Serializable 需要保证反序列化不会创建可变实例,必要时重写 readResolve()。

🆚 容易踩坑总结

表格

坑点

说明

只加 final 在成员上

对可变引用无效,final 只是不能重新赋值引用,对象内部还能改

不防御拷贝

外部拿到同一个引用,修改内部状态

类不加 final

子类继承后重写方法,破坏不可变约定

返回原始集合(ArrayList)

外部 get 之后 add/remove 修改内部数据

✨ Java 新方案:Record(Java 16+)

Java 16 提供 record 语法,自动生成极简不可变载体类,默认:

  • 类隐式 final

  • 成员隐式 final

  • 自动构造器、equals、hashCode、toString、getter

但是!record 不会自动对可变引用做防御拷贝! 这是高频面试坑!

// Record 写法,适合纯数据载体
public record UserRecord(Long id, String name, Date birth) {}

⚠️ 上面这个 UserRecord 不是安全不可变类!因为 Date birth 是可变对象,构造直接复用引用,必须手动在紧凑构造器里防御拷贝:

public record UserRecord(Long id, String name, Date birth) {
    // 紧凑构造器
    public UserRecord {
        birth = birth == null ? null : new Date(birth.getTime());
    }
}

📌 和 String 不可变性关联

String 底层(Java9+)是 private final byte[] value; String 符合不可变类规范:类 final、无 setter、修改操作 (substring /replace)都是返回新对象,不修改自身。


本文原创作者:易君召,详见:https://www.yijunzhao.cc/about,转载请注明出处。

原文链接 https://www.yijunzhao.cc/archives/java-immutable-class-implementation

欢迎访问 https://www.yijunzhao.cc/

https://www.yijunzhao.cc/