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 知识问答

  1. final 是什么意思?
  2. final 可以修饰哪些 Java 程序元素?
  3. final 修饰类意味着什么?
  4. final 修饰方法意味着什么?
  5. final 修饰变量意味着什么?
  6. final 类是否自动代表对象不可变?
  7. final 引用与不可变对象是不是同一个概念?
  8. final 基本类型变量限制的是什么?
  9. final 引用变量限制的是什么?
  10. final 数组为什么仍然可以修改元素?
  11. 什么是 blank final?
  12. blank final 实例字段通常可以在哪里完成初始化?
  13. static final 字段常用于表达什么?
  14. 常量通常采用什么命名规范?
  15. 什么叫魔法数字?
  16. 为什么常量有利于代码维护?
  17. JLS 中 constant variable 的严格定义是什么?
  18. 为什么 static final User USER = new User() 不属于严格 JLS constant variable?
  19. 为什么不能把所有 static final 都描述成“宏替换”?
  20. 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]);
    }
}

回答:

  1. arr[1] = 99 为什么允许?
  2. 为什么不能重新给 arr 赋另一个数组?
  3. final 在这里锁定的到底是什么?
  4. 最终输出什么?

阅读:

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();
    }
}

回答:

  1. user.name 能否变化?
  2. 为什么?
  3. user 自身能否重新赋值?
  4. 这是否证明 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() {
    }
}

回答:

  1. 问题是什么?
  2. 为什么 final 字段不能保持未初始化状态?
  3. 至少给出一种正确设计方式。

7.5 综合训练

为一个博客系统设计配置类:

BlogConstants

至少包含:

SITE_NAME
DEFAULT_PAGE_SIZE
MAX_PAGE_SIZE
MAX_TITLE_LENGTH
MAX_SEARCH_KEYWORD_LENGTH

要求:

  1. 判断字段应该使用什么修饰符。
  2. 采用统一命名规范。
  3. 禁止创建无意义实例。
  4. 禁止继承配置类。
  5. 写一个分页判断方法使用这些常量。
  6. 分析相比直接在代码中写 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 = 不能改

却说不清:

类不能改什么、
方法不能改什么、
变量不能改什么、
引用为什么还能修改对象,

那么这一章还没有真正掌握。