final 与常量 | JavaSE
final 与常量
一、学习目标
学完本章,你应该能够:
- 解释
final关键字的核心含义。 - 掌握
final修饰类、方法、变量时分别限制什么。 - 理解 final 类为什么不能被继承。
- 理解 final 方法为什么不能被子类重写。
- 理解 final 基本类型变量只能确定一次值。
- 理解 final 引用变量限制的是引用本身,而不是自动冻结对象内部状态。
- 理解 final 数组引用仍然可以修改数组元素。
- 理解 blank final(空白 final)成员变量的初始化方式。
- 区分
final引用与不可变对象(Immutable Object)。 - 掌握
static final常量的常见工程写法。 - 掌握常量命名规范。
- 准确区分工程语境中的“常量”与 JLS 中更加严格的 Constant Variable。
- 避免把所有
static final都简单理解为“编译器宏替换”。
二、核心知识
2.1 final 是什么
final 是 Java 中的重要关键字。
可以理解为:
最终确定、不再允许对应语言结构发生特定变化。
当前阶段重点学习它修饰:
类
方法
变量
三种情况。
建立最重要的总表:
| final 修饰 | 核心效果 | | ---------- | ------------------------ | | 类 | 类不能再被继承 | | 方法 | 方法不能再被子类重写 | | 变量 | 变量只能完成一次有效赋值 |
所以:
final 修饰不同元素
具体“不能变”的东西并不一样。
2.2 final 修饰类
例如:
public final class Animal {
}
此时:
Animal
是:
final 类。
不能:
public class Dog extends Animal {
}
因为 Java 已明确规定:
final 类不能被子类扩展。
2.3 final 类表达什么设计意图
当设计者写:
final class Xxx {
}
是在明确表达:
这个类的类型实现到这里结束,不希望再通过继承创建新的子类类型。
例如 JDK 中:
String
就是非常典型的 final 类。
所以:
class MyString extends String {
}
是不允许的。
2.4 final 类并不意味着对象不能修改
这是一个重要区别。
假设:
final class User {
private String name;
public void setName(String name) {
this.name = name;
}
}
虽然:
User 类不能被继承
但:
User user = new User();
user.setName("张三");
user.setName("李四");
如果类本身允许修改内部状态,这仍然完全可能。
因此:
final class ≠ immutable object
final 类解决的是:
是否允许继承
不是自动解决:
对象内部状态是否可修改
2.5 final 修饰方法
例如:
public class Animal {
public final void sleep() {
System.out.println("sleep");
}
}
子类:
public class Dog extends Animal {
}
不能:
@Override
public void sleep() {
}
因为父类已经声明:
final
表示:
这个方法实现不允许被子类重新定义。
2.6 final 方法与方法重写
前面已经学习:
父类方法
↓
子类 @Override
↓
多态行为
但是一旦:
public final void run() {
}
就意味着设计者主动关闭:
run() 的进一步重写扩展点
因此:
final method
和:
method overriding
具有直接联系。
2.7 为什么某些方法需要 final
假设父类中存在一个核心流程:
public final void verifyIdentity() {
...
}
如果系统要求:
所有子类必须遵循完全一致的验证流程。
那么禁止子类重写可能是合理的设计选择。
所以 final 并不是:
“为了限制程序员发挥”。
而是用来表达:
某些类型或行为已经被设计为不可再扩展。
2.8 final 修饰变量
例如:
final int age = 18;
之后不能:
age = 20;
因为:
final 变量只能被赋值一次。
再例如:
final double PI = 3.1415926;
之后:
// PI = 3.14;
不能再次赋值。
2.9 final 不代表必须“声明时立即赋值”
例如:
final int age;
age = 18;
只要 Java 的确定赋值规则能够证明:
第一次赋值以前变量确实未被赋值
这种形式可以成立。
赋值完成以后:
// age = 20;
就不允许了。
因此:
final 的核心不是“必须写在同一行”,而是变量最终只能确定一次值。
2.10 final 成员变量
例如:
public class User {
private final String id;
public User(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
这里:
id
声明时没有立即赋值。
而是在构造器:
this.id = id;
中完成第一次初始化。
这种形式通常称为:
blank final / 空白 final
它非常有实际意义。
例如:
用户 ID
订单编号
对象创建后不允许替换的依赖引用
都可能采用类似思想。
2.11 final 成员必须保证初始化
例如:
public class User {
private final String id;
public User() {
}
}
这里:
id
没有得到确定值。
编译器会报错。
必须保证每一个可以完成对象创建的构造路径最终都能让:
id
获得一次有效赋值。
例如:
public User() {
this.id = "DEFAULT";
}
或者:
public User(String id) {
this.id = id;
}
2.12 final 修饰方法参数
例如:
public static void print(
final int value
) {
System.out.println(value);
// value = 100;
}
这里:
value
作为形参已经在调用方法时获得值。
由于声明:
final
方法体内部就不能重新给这个参数赋值。
2.13 final 修饰基本类型变量
例如:
final int number = 10;
变量中保存的是:
int 数值 10
所以 final 限制以后:
number 中保存的基本类型值不能被再次替换
不能:
number = 20;
2.14 final 修饰引用类型变量
这是本章最容易出错的知识点。
例如:
final int[] arr = {
11, 22, 33
};
不能:
arr = new int[] {
1, 2, 3
};
因为:
arr 保存的是数组对象引用
final 限制:
arr 不能再保存另一个数组对象引用。
但是:
arr[1] = 99;
是允许的。
因为:
修改的是数组对象内部元素,而不是让
arr引用另一个数组。
2.15 final 引用图解
final int[] arr = {11, 22, 33};
可以理解:
arr
│
│ final:这条引用关系不能重新指向其他数组
▼
┌─────────────┐
│ 11 22 33 │
└─────────────┘
允许:
arr[1] = 99;
变成:
arr
│
▼
┌─────────────┐
│ 11 99 33 │
└─────────────┘
但是不能:
arr
│
└──────────────→ 另一个数组
2.16 final 引用不等于不可变对象
例如:
public class User {
private String name;
public User(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
}
使用:
final User user =
new User("张三");
不能:
user = new User("李四");
但仍然可以:
user.setName("李四");
因为:
final
限制的是:
user 引用
不是自动限制:
User 对象内部的 name
所以:
final reference ≠ immutable object
2.17 什么是常量
在本课程和日常 Java 工程语境中,经常把:
static final
成员字段称为:
常量。
例如:
public class Constants {
public static final String SYSTEM_NAME =
"星雨笔录";
public static final int MAX_RETRY_COUNT =
3;
public static final int DEFAULT_PAGE_SIZE =
20;
}
调用:
System.out.println(
Constants.SYSTEM_NAME
);
因为:
static
↓
属于类
final
↓
赋值后不能再次改变
所以特别适合表示:
固定配置值
固定比例
固定限制
状态编码
系统名称
等。
2.18 常量为什么通常 static final
假设:
public final int MAX_RETRY = 3;
如果它属于每个对象:
每创建一个对象
似乎都拥有相同的固定值
通常没有必要。
更自然的是:
public static final int MAX_RETRY = 3;
表示:
MAX_RETRY
属于整个 Constants 类
而且不可重新赋值
调用:
Constants.MAX_RETRY
即可。
2.19 常量命名规范
Java 工程中常见规范:
大写英文字母
多个单词使用下划线连接
例如:
SYSTEM_NAME
MAX_RETRY_COUNT
DEFAULT_PAGE_SIZE
DEFAULT_TIMEOUT_SECONDS
不推荐:
systemName
maxRetryCount
用于这类真正的共享固定常量。
这样读代码时可以迅速识别:
这是一个常量,而不是普通变量。
2.20 Constants 类
可以把系统中一组相关常量集中组织:
public final class Constants {
private Constants() {
}
public static final String SYSTEM_NAME =
"星雨笔录";
public static final int MAX_RETRY_COUNT =
3;
public static final int DEFAULT_PAGE_SIZE =
20;
}
这里同时使用了:
final class
private constructor
static final fields
分别表达:
final class
→ 不希望被继承
private constructor
→ 不希望创建无意义实例
static final
→ 提供类级固定值
2.21 Java 语言规范中的“constant variable”更严格
日常教学中经常说:
static final就是常量。
这作为工程入门习惯很好理解。
但是 Java Language Specification 对:
constant variable
有更加严格的技术定义。
它要求变量:
1. 是 final
2. 类型是基本类型或 String
3. 使用常量表达式初始化
例如:
public static final int MAX_SIZE = 100;
属于典型编译期常量变量。
再如:
public static final String APP_NAME =
"星雨笔录";
也可以满足。
2.22 static final 对象引用不一定是 JLS constant variable
例如:
public static final User DEFAULT_USER =
new User("guest");
工程上经常仍然会口语称:
static final 常量
因为这个引用不能再重新赋值。
但从 JLS 严格术语来看:
User
既不是基本类型,也不是 String。
因此:
DEFAULT_USER
并不属于 JLS 所定义的:
constant variable。
这两个语境要区分。
三、使用方法
3.1 final 类
public final class SecurityConfig {
public void check() {
System.out.println(
"Security check"
);
}
}
不能:
class MySecurityConfig
extends SecurityConfig {
}
3.2 final 方法
public class Account {
public final void verifyPassword() {
System.out.println(
"统一密码验证"
);
}
}
子类:
public class VipAccount
extends Account {
// 不能重写 verifyPassword()
}
3.3 final 字段通过构造器初始化
public class Order {
private final String orderId;
public Order(String orderId) {
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
创建以后:
orderId
不能被重新赋值。
这比原始教学示例中:
private final String name = "...";
更加能够体现 final 实例字段的实际价值。
3.4 Constants 工具式设计
public final class SystemConstants {
private SystemConstants() {
}
public static final String SYSTEM_NAME =
"星雨笔录";
public static final int MAX_LOGIN_RETRY =
5;
public static final int DEFAULT_PAGE_SIZE =
20;
}
使用:
System.out.println(
SystemConstants.SYSTEM_NAME
);
无需创建:
new SystemConstants()
四、原理与进阶
4.1 final 真正限制的是“重新赋值”
对于:
final User user =
new User("A");
final 直接限制的是:
变量 user 自己保存的引用值不能重新改变
而不是给堆对象加了一层:
禁止修改
所以:
user.setName("B");
是否允许,取决于:
User 类本身有没有提供修改状态的能力
4.2 final 与 immutable 是两个维度
final reference
final User user
解决:
这个变量不能重新指向另一个 User。
immutable object
解决:
这个对象创建以后,内部可观察状态不能被修改。
因此可能存在:
final 引用
+
可变对象
例如:
final ArrayList<String> list =
new ArrayList<>();
list.add("Java");
引用没变。
对象内容变了。
4.3 final 类也不是 immutable 的充分条件
final class Counter {
private int count;
public void add() {
count++;
}
}
虽然类不能被继承,但:
count
仍然不断改变。
所以:
final class
只意味着:
不允许继承。
不能根据这一点推导:
对象不可变。
4.4 “所有 static final 都会宏替换”是不准确的
原始课程使用了非常直观的说法:
常量编译后会被“宏替换”。
这个说法可以帮助初学者理解某些:
static final int
static final String
编译期常量可能被直接内联到使用方代码。
但不能理解成:
任意
static final字段都会被 Java 编译器无条件替换成某个字面量。
例如:
public static final User USER =
new User("guest");
它不是编译期常量变量。
再例如某些:
static final
字段的初始化表达式本身也不是编译期常量表达式。
所以更准确的口径应该是:
符合 JLS constant variable / constant expression 条件的值,在编译和链接语义中可能直接以内联常量形式使用;不要把 Java 简化成 C/C++ 预处理器式“宏替换”。
4.5 为什么修改公共编译期常量可能有版本问题
假设库:
public static final int MAX = 10;
另一个已经编译完成的程序使用:
System.out.println(MAX);
对于真正的编译期常量,使用方字节码中可能已经记录:
10
如果库后来只把:
MAX
改成:
20
而使用方没有重新编译,可能仍然看到旧值。
所以:
public compile-time constant 的修改还涉及二进制兼容性问题。
现阶段只需要建立这个意识,不要求深入字节码。
4.6 blank final 与对象初始化
例如:
public class Student {
private final String id;
public Student(String id) {
this.id = id;
}
}
id 声明时虽然没有值,但编译器能够证明:
每个 Student 对象构造结束以前
id 都完成一次确定赋值
因此合法。
这与 Java 的:
definite assignment
规则有关。
五、实践应用
5.1 固定系统配置
例如:
public final class BlogConstants {
private BlogConstants() {
}
public static final String SITE_NAME =
"星雨笔录";
public static final int PAGE_SIZE =
20;
public static final int MAX_SEARCH_LENGTH =
100;
}
好处:
- 避免魔法数字;
- 统一修改;
- 提高可读性;
- 明确表达固定业务语义。
例如:
if (keyword.length()
> BlogConstants.MAX_SEARCH_LENGTH) {
...
}
明显比:
if (keyword.length() > 100) {
...
}
更容易理解:
100 到底代表什么
5.2 状态码
例如:
public final class StatusCode {
private StatusCode() {
}
public static final int SUCCESS = 200;
public static final int BAD_REQUEST = 400;
public static final int NOT_FOUND = 404;
}
当前阶段可以利用常量减少:
裸数字
但如果数据本质属于:
一组有限固定状态
后面的:
02-14 枚举
通常会提供更强的类型表达能力。
所以常量不是所有有限状态问题的终点。
5.3 final 对象标识
例如:
public class Article {
private final String id;
private String title;
public Article(
String id,
String title
) {
this.id = id;
this.title = title;
}
public String getId() {
return id;
}
public void setTitle(String title) {
this.title = title;
}
}
这里:
id
创建以后不允许重新赋值。
但:
title
允许变化。
这说明:
对象内部不同字段可以拥有不同生命周期和可变性要求。
六、常见问题
6.1 final 可以修饰什么?
本章重点:
类
方法
变量
6.2 final 类意味着什么?
不能再被其他类继承。
6.3 final 方法意味着什么?
不能被子类重写。
更严格地说,对可隐藏的 class/static method,final 同样阻止子类重新声明相应隐藏行为。
6.4 final 变量是不是必须在声明时赋值?
不是。
可以使用:
blank final
在构造器等合法位置完成唯一一次确定赋值。
6.5 final 基本类型限制什么?
限制变量保存的基本数据值不能再次被替换。
6.6 final 引用限制什么?
限制:
引用变量不能重新指向其他对象。
不自动限制:
当前对象内部状态。
6.7 final 数组元素还能修改吗?
可以。
例如:
final int[] arr = {1, 2, 3};
arr[0] = 100;
合法。
不能:
arr = new int[] {4, 5, 6};
6.8 final User 就表示 User 不可变吗?
不是。
final User user
只说明:
user 不能重新指向另一个 User
对象是否可变由 User 类自己的设计决定。
6.9 static final 就一定是 JLS 严格意义的 constant variable 吗?
不是。
JLS 的 constant variable 要求:
final
+
基本类型或 String
+
常量表达式初始化
日常工程语境中的“static final 常量”范围通常更宽。
6.10 常量为什么全部大写?
这是 Java 常见命名约定,用来帮助开发者快速识别共享固定值。
多个单词通常:
UPPER_SNAKE_CASE
例如:
MAX_RETRY_COUNT
6.11 常量的价值只是性能吗?
不是。
更重要的是:
- 表达业务语义;
- 消除魔法值;
- 集中维护;
- 提升代码可读性。
6.12 Java 常量是不是和 C 的宏完全一样?
不是。
“宏替换”只能作为非常粗略的入门比喻。
Java 有自己的:
constant variable
constant expression
编译器
class file
二进制兼容性
规则。
七、练习与验收
7.1 知识问答
final是什么意思?- final 可以修饰哪些 Java 程序元素?
- final 修饰类意味着什么?
- final 修饰方法意味着什么?
- final 修饰变量意味着什么?
- final 类是否自动代表对象不可变?
- final 引用与不可变对象是不是同一个概念?
- final 基本类型变量限制的是什么?
- final 引用变量限制的是什么?
- final 数组为什么仍然可以修改元素?
- 什么是 blank final?
- blank final 实例字段通常可以在哪里完成初始化?
- static final 字段常用于表达什么?
- 常量通常采用什么命名规范?
- 什么叫魔法数字?
- 为什么常量有利于代码维护?
- JLS 中 constant variable 的严格定义是什么?
- 为什么
static final User USER = new User()不属于严格 JLS constant variable? - 为什么不能把所有 static final 都描述成“宏替换”?
- final 与后面的不可变对象设计有什么区别?
7.2 代码阅读
阅读:
public class FinalRead {
public static void main(
String[] args
) {
final int[] arr = {
11, 22, 33
};
arr[1] = 99;
// arr = new int[] {
// 1, 2, 3
// };
System.out.println(arr[1]);
}
}
回答:
arr[1] = 99为什么允许?- 为什么不能重新给
arr赋另一个数组? - final 在这里锁定的到底是什么?
- 最终输出什么?
阅读:
class User {
String name;
}
public class Test {
public static void main(String[] args) {
final User user =
new User();
user.name = "张三";
user.name = "李四";
// user = new User();
}
}
回答:
user.name能否变化?- 为什么?
user自身能否重新赋值?- 这是否证明 final 对象不是不可变对象?
7.3 手写代码
任务一:Constants
设计:
Constants
保存:
系统名称
最大重试次数
默认分页大小
默认超时时间
要求:
- 使用
static final。 - 使用大写下划线命名。
- 构造器私有化。
- 类本身禁止继承。
- 通过
类名.常量名使用。
任务二:final ID
设计:
User
字段:
id
name
要求:
id创建后不能重新赋值。name可以正常修改。id必须通过构造器初始化。- 不允许给
id提供 setter。
任务三:final 引用
分别创建:
final int
final int[]
final User
通过代码证明:
基本类型值不能重新赋值
数组引用不能重新赋值,但元素可变
对象引用不能重新赋值,但对象状态可能可变
7.4 Debug
下面代码为什么不能编译?
final class Animal {
}
class Dog extends Animal {
}
指出违反的规则。
下面代码:
class Parent {
public final void run() {
}
}
class Child extends Parent {
@Override
public void run() {
}
}
解释编译错误原因。
下面代码:
class User {
private final String id;
public User() {
}
}
回答:
- 问题是什么?
- 为什么 final 字段不能保持未初始化状态?
- 至少给出一种正确设计方式。
7.5 综合训练
为一个博客系统设计配置类:
BlogConstants
至少包含:
SITE_NAME
DEFAULT_PAGE_SIZE
MAX_PAGE_SIZE
MAX_TITLE_LENGTH
MAX_SEARCH_KEYWORD_LENGTH
要求:
- 判断字段应该使用什么修饰符。
- 采用统一命名规范。
- 禁止创建无意义实例。
- 禁止继承配置类。
- 写一个分页判断方法使用这些常量。
- 分析相比直接在代码中写
20、100、255有什么优势。
再设计:
Article
其中:
id 创建后不允许修改
title 可以修改
content 可以修改
通过该对象解释:
final 字段与整个对象不可变为什么不是同一个概念。
7.6 本章验收
关闭资料,确认自己能够:
- [ ] 解释 final 三种核心用途。
- [ ] 手写 final 类。
- [ ] 手写 final 方法。
- [ ] 手写 final 局部变量。
- [ ] 使用构造器初始化 blank final 字段。
- [ ] 解释 final 基本类型。
- [ ] 解释 final 引用类型。
- [ ] 分析 final 数组代码。
- [ ] 区分 final reference 与 immutable object。
- [ ] 独立设计 Constants 类。
- [ ] 使用 UPPER_SNAKE_CASE 命名常量。
- [ ] 说出 JLS constant variable 的严格条件。
- [ ] 解释为什么并非所有 static final 都是编译期常量。
- [ ] 不再把 Java 常量简单理解成 C 风格宏。
如果你只记住:
final = 不能改
却说不清:
类不能改什么、
方法不能改什么、
变量不能改什么、
引用为什么还能修改对象,
那么这一章还没有真正掌握。