在Java中如何理解可变对象与不可变对象

来源:HTML教程作者:椎名光头衔:网络博主
导读:本期聚焦于小伙伴创作的《在Java中如何理解可变对象与不可变对象》,敬请观看详情。为什么修改一个Java对象后,另一个引用也跟着变了?这往往是因为踩中了可变对象的共享引用陷阱。可变对象允许内部状态被修改,而不可变对象一旦创建便无法更改,例如String和Integer。理解两者差异有助于在并发编程中避免数据竞争,也能减少因意外修改导致的逻辑错误。本文从内存模型、线程安全、设计权衡等角度说明如何识别并合理使用它们,并给出创建不可变类的标准写法与常见误区。

在Java中,对象从状态是否可被修改的角度可以分为可变对象与不可变对象。可变对象在创建之后,其内部的字段值仍然可以通过公开的方法或者直接访问被更改;而不可变对象在构造完成之后,对外不提供任何修改自身状态的方式,任何看起来像修改的操作实际上都返回了一个新的对象实例。理解这两类对象的本质区别,是写出健壮、可维护以及线程安全代码的基础。

在Java中如何理解可变对象与不可变对象

从内存与引用角度看可变与不可变

当我们说一个对象是「可变」的时候,核心含义是:在堆内存中该对象实例的数据内容可以被改变,而栈上的多个引用变量可以指向同一个堆实例。例如有一个普通的User类,它包含可写的name字段,那么通过任意一个引用调用setName方法,都会修改那一块共享的堆内存,其他引用立刻就能观察到变化。这种共享可变状态在单线程里方便,但在多线程里极易引发问题。

不可变对象则走了另一条路。以java.lang.String为例,字符串底层使用final字符数组并且不对外暴露修改入口,所有substringconcat等操作都返回新的String实例。也就是说,原来的对象在内存中纹丝不动,变化通过「新建对象」来体现。这样做虽然会带来一定的对象创建开销,但换来了引用透明性:只要拿到一个引用,就可以放心地认为它代表的值永远不变。

从JVM内存模型层面看,可变对象如果未被正确同步,一个线程的写入对其他线程可能不可见,甚至看到部分构造的状态;不可变对象由于状态固定,只要构造过程本身是安全的(没有this引用逸出),发布之后不需要额外同步就能被所有线程安全共享。这也是为什么许多并发框架都推荐尽量使用不可变数据载体。

如何编写一个标准的不可变类

在Java中把一个类设计成不可变,需要遵循几条硬性规则。第一,类本身应当用final修饰,防止被继承后通过重写方法破坏不变性约束。第二,所有字段必须是privatefinal,并在构造器中完成全部初始化。第三,不提供任何setter或者其他能修改字段的方法。第四,如果字段引用了可变对象(例如一个List),那么在构造时应当进行防御性拷贝,在返回时也要返回拷贝而非原引用。

下面给出一个符合规范的不可变类示例,其中对传入的列表做了深一层保护,避免外部代码后续修改影响内部状态:

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

public final class ImmutableOrder {
    private final long id;
    private final List<String> items;

    public ImmutableOrder(long id, List<String> items) {
        this.id = id;
        // 防御性拷贝,避免外部列表被修改
        this.items = Collections.unmodifiableList(new ArrayList<>(items));
    }

    public long getId() {
        return id;
    }

    public List<String> getItems() {
        // 返回不可修改视图,防止调用方改内部集合
        return items;
    }
}

上面的代码里,Collections.unmodifiableList返回的是一个包装器,它阻止了对原列表的增删改,但并不阻止原列表引用本身被替换,所以配合final字段才是完整的保护。如果字段是基本类型或真正不可变的引用(如String),则不需要这么麻烦的拷贝逻辑。

与之相对,可变对象只要去掉final、提供setter即可。虽然写起来简单,但在方法间传递时,要时刻警惕「谁改了我的数据」。一种常见实践是:对内部状态只返回拷贝,或者明确在文档里声明该方法会修改入参,从而降低协作成本。

可变与不可变对象的适用场景及权衡

不可变对象最适合用作值对象、配置项、多线程间传递的消息以及缓存的key。例如在HashMap中做key的String或自定义不可变ID就非常安全;一旦key可变,hashCode变化会导致无法正确取值。又比如在函数式风格代码里,不可变数据让流水线每一步都清晰可预测,不会因为上游偷偷改了集合而让下游出错。

可变对象则在需要频繁修改、且对象较大或生命周期较长时更有优势。比如一个实时游戏里的角色坐标,如果每移动一次都new一个新坐标对象,GC压力会很大;此时用可变Point并在原地更新反而更高效。同理,构建复杂报表时,先用可变集合累积数据,最后包装成不可变视图对外发布,是兼顾性能与安全的常用模式。

在实际架构中,一个稳妥的策略是:对外接口和跨线程边界传递数据时使用不可变对象,进程内部、同一线程的算法细节中合理使用可变对象以减少开销。同时要警惕「伪不可变」——例如字段是final但里面套着一个可变的HashMap,若直接把该map的引用给出去,对象实际上还是可变的。厘清这些边界,才能真正在Java中理解并运用好可变与不可变对象。

mutable_objectimmutable_objectJava_final修改时间:2026-08-16 01:52:12

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。