Sunday面试指南

Java 的 final 有什么作用?final 修饰对象引用,就代表对象不可变吗?

下面是一段教学用的模拟面试。

🧑‍💻 面试官: final 修饰一个对象,这个对象还能改变吗?

🙋‍♂️ 我: final 表示常量,所以不能改。

🧑‍💻 面试官: 一个 final 的 List,还能 add 吗?

🙋‍♂️ 我: 引用不能换,列表里面的元素可能还能改。

🧑‍💻 面试官: 那你要做不可变配置类,只把字段都加上 final,够不够?

要区分两个问题:「这个引用能不能换」和「沿着引用找到的内容能不能改」。

面试速答(60 秒版)

final 放在不同位置,约束不同。变量或字段被 final 修饰,表示赋值完成后不能再次赋值;方法被 final 修饰,不能被重写;类被 final 修饰,不能被继承。

对于对象引用,不能重新赋值,不等于对象内容不可变。final 的集合引用,仍可能允许添加元素;集合里的对象,也可能有可修改的字段。

设计不可变对象,还需要控制内部状态的修改入口。可变输入要考虑防御性复制,返回值不能暴露内部可变对象,对嵌套对象也要检查。

final 字段还有与正确构造相关的内存模型保证,但不能把它说成任意操作都线程安全,也不能在构造期间随意让 this 逃逸。

final 固定引用,不固定内容

图:final 固定引用,不固定内容。

知识点详解:引用固定了,不代表内容也固定了

先看清被限制的是哪一层

假设配置类有一个 final 列表字段,用它保存允许访问的域名。

字段保存的是指向列表的引用。final 限制的是以后不能把这个字段指向另一张列表。原来那张列表如果是可变集合,仍可以增加或删除域名。

因此,我们可以把关系拆成三层:字段、集合、集合里的元素。第一层不能改,不会自动让第二层和第三层都不可变。JLS 的 final 变量说明区分了引用与对象状态。

输入和输出都可能把内部状态暴露出去

假设构造函数直接保存调用方传进来的列表。对象自己没有提供修改方法,但调用方手里还握着原列表,照样能从外面修改它。

如果 getter 又直接返回内部可变列表,拿到返回值的人也能修改配置。所以,“没有 setter”不是完整的不可变设计。

通常要在接收输入时建立自己的快照,再根据需求返回不可修改的结果。如果复制的是元素引用,而元素本身还能改,这种浅复制仍然不够。比如列表不能增删,但某个配置项的 timeout 字段可以变化,内部状态还是变了。

外部别名,也能改内部状态

图:外部别名,也能改内部状态。

只读视图与快照也不一样

不可修改的集合视图,可以禁止通过这个视图增删,但底层集合若被别处修改,视图可能仍会反映变化。

快照则表达“记录创建时的内容”,不跟着原集合变化。我们选哪种,要看业务需要的是限制修改入口,还是固定一份配置版本。

这类区别比背“final 是常量”更有用:它直接决定配置能否在并发请求之间保持稳定。

final 的初始化保证,不能无限放大

在对象被正确构造等条件下,final 字段具有特殊的初始化安全语义。它有助于其他线程看到构造时建立的状态,具体条件见 JLS final 字段语义。

但构造函数还没结束,就把 this 放进共享位置、启动线程或交给回调,会破坏我们依赖的构造前提。对象之后的可变操作,也不能因为字段带 final 就免掉同步设计。

这里讲 Java 本身的语言规则,不把 TypeScript readonly、Python Final 当成等价实现。前者主要是类型检查约束,后者也不能自动把运行时对象冻结。

面试官继续追问

final 类一定不可变吗?

不是。禁止继承,不会禁止类自己的方法修改字段指向的对象。它可以帮助控制扩展行为,但不是不可变性的充分条件。

深复制越深越好吗?

不一定。先明确要保护哪些状态,再选择不可变元素、复制或受控共享。无区别地复制庞大对象图,会增加成本,也可能改变原本需要保留的共享关系。

面试速记卡

  • final 引用:不能再指向另一个对象。
  • 对象内容:仍可能可变,要继续检查下一层。
  • 不可变设计:控制修改入口,检查输入、输出和嵌套状态。
  • 视图与快照:禁止从这里修改,不等于内容永远不变。
  • 构造发布:final 有条件的初始化保证,不是通用并发保险。

公司面试真题

这道题暂未收录可核验的公司真题来源。你可以先阅读本文解析,或浏览已收录的公司面试真题。

浏览公司面试真题 →
简历汪永久免费在线制作简历,模板直接套用、导出无水印,永久免费、下载免费,不需要付费解锁任何功能。去写简历