final关键字

final关键字

不同的使用环境下,final关键字的含义有细微差别,但通常它指“这是无法改变的”。

final数据 #

final关键字通常用来表示一块数据是恒定不变的:

常量 #

  • 比如一个永不改变的编译时常量 ---> 对应前文所谓静态常量:

    • public final double PI = 3.14

    • 如果使用static final来修饰变量,那么它就是一个标准的编译时常量(静态常量)。

    • public static final double PI = 3.14

    • 在Java中,编译时常量必须是基本数据类型字符串,声明时必须赋值。

  • 一个在运行时被初始化的值,你不希望它被改变

    • public final int RANDOM = new Random().nextInt(47)

一个既是final也是static的域只占用一段不能改变的存储空间

当对非基本数据类型使用final时,其含义稍微有点变化,其使引用恒定不变,这就是说被final修饰的对象引用无法使其指向另一个对象,但是对象内容却是可以被修改的。

 1public class FinalProperty {
 2
 3    final int a = 1;
 4
 5    final String s = "java";
 6
 7    final int[] a_array = new int[3];
 8
 9    static final String[] gender = new String[] { "male", "female" };
10
11    void test() throws Exception {
12        // illegal, cannot modify final fields
13        // a--
14        // s= "c++";
15
16        // use reflect can change final fields (before jdk 9)
17        // Field value = String.class.getDeclaredField("value");
18        // value.setAccessible(true);
19        // char[] chars = (char[]) value.get(s);
20        // chars[0] = 'J';
21        // System.out.println(s);
22
23        // object content can be modified
24        a_array[0] = 9;
25        a_array[1] = 2;
26        a_array[2] = 5;
27
28        Arrays.sort(a_array);
29
30        System.out.println(Arrays.toString(a_array));
31
32        String[] Gender = new String[] { "male", "female" };
33        // not allowed! cannot reference final
34        // property to a new reference!
35        // gender = Gender;
36        gender[0] = "MALE";
37        System.out.println(Arrays.toString(gender));
38
39    }
40
41    public static void main(String[] args) throws Exception {
42        new FinalProperty().test();
43    }
44}
45
46/*
47 * output:
48 * [2, 5, 9]
49 * [MALE, female]
50 *///:~

Unable to make field private final byte[] java.lang.String.value accessible

这个限制,即无法通过反射访问java.lang.String类的私有 value 字段,是在Java 9中引入的模块化系统 (Project Jigsaw) 的一部分。

更具体地说,这是由强封装(strong encapsulation) 机制强制执行的。 在Java 9之前,虽然 value 字段是私有的,但仍然可以通过反射 API 的 setAccessible(true)方法来访问和修改它。 Java 9引入了模块的概念,并且默认情况下,模块不会导出其内部实现细节。java.lang包属于java.base模块,而java.base模块默认情况下不会导出java.lang.String的内部字段。

因此,从Java 9开始,尝试通过反射访问java.lang.String 的 value 字段会抛出java.lang.IllegalAccessException异常,除非你采取一些特殊的措施来绕过模块系统的限制 (例如,使用 --add-opens 命令行参数)

Java允许声明一个空白final域,但是这个final域在使用前必须使用构造器(常见)或表达式初始化。

 1// 在构造器中初始化 final 域
 2public class MyClass {
 3    private final int x;
 4
 5    public MyClass(int value) {
 6        x = value; // 在构造器中初始化 final 域
 7    }
 8
 9    public int getX() {
10        return x;
11    }
12}
13// 在实例初始化块中初始化 final 域
14public class MyClass {
15    private final int x;
16
17    {
18        x = 10; // 在实例初始化块中初始化 final 域
19    }
20
21    public MyClass() {
22        // 构造器可以为空,因为 x 已经在实例初始化块中初始化了
23    }
24
25    public int getX() {
26        return x;
27    }
28}
29// 在静态初始化块中初始化 final 域
30public class MyClass {
31    private static final int x;
32
33    static {
34        x = 10; // 在静态初始化块中初始化静态 final 域
35    }
36
37    public static int getX() {
38        return x;
39    }
40}

参数 #

此外,Java允许参数列表中将参数指明为final,这意味着你无法在方法中更改参数引用所指向的对象,但是对象状态也是可变的

注意此处的表述。作为对比,Java引用对象作为参数时, 参数引用指向的对象是可以改变的(尽管这个改变不能应用到对象引用上---对象引用不可变)

更新于 2025-05-06

实际上表述的意思是一样的,对象可以改变(状态),对象的引用(地址)不可变。

Employee2使用之前的代码:

 1public class FinalParam {
 2
 3    static void with(final Employee e) {
 4        e.raiseSalary(3);
 5        System.out.println("salary of e = " + e.getSalary());
 6    }
 7
 8    static void swap(final Employee j, final Employee k) {
 9        Employee tmp = j;
10        // check error, final var assign is not allowed
11        // k = temp;
12        // j = k;
13    }
14
15    public static void main(String[] args) {
16        Employee x = new Employee("ali", 1000);
17        with(x);
18        System.out.println("salary of x = " + x.getSalary());
19    }
20}
21
22/* output:
23 * salary of e = 3000
24 * salary of x = 3000
25 */// :~

final参数一般用来向匿名内部类中传递数据。

实际上,Java并没有提供任何对象恒定不变的途径,虽然可以通过编程取得对象恒定不变的效果单例

final方法--阻止继承 #

当将一个方法声明为final时,其主要作用就是锁定方法,阻止继承,这往往是出于设计的考虑。

需要特殊说明的是:如果一个方法是private的,那么它就被隐式地声明为final了,因为由于无法取用private方法,也就无法覆盖之。

参考如下代码:

 1public class FinalMethodT {
 2    public static void main(String[] args) {
 3        FinalMethodExt x = new FinalMethodExt();
 4        x.f();
 5        x.g();
 6        x.p();
 7        System.out.println("----");
 8        // upcast
 9        FinalMethod y = x;
10        y.f();
11        y.g();
12        // y.p(); // can't access
13        System.out.println("----");
14        FinalMethod z = new FinalMethod();
15        z.f();
16        z.g();
17        // z.p(); // can't access
18    }
19}
20
21class FinalMethod {
22    void f() {
23        System.out.println("f()");
24    }
25
26    final void g() {
27        System.out.println("g()");
28    }
29
30    private void p() {
31        System.out.println("p()");
32    }
33}
34
35class FinalMethodExt extends FinalMethod {
36
37    @Override
38    void f() {
39        // super.f();
40        System.out.println("ext f()");
41    }
42    // cannot override
43    // final void g(){ System.out.println("ext g()"); }
44
45    final void p() {
46        System.out.println("ext p()");
47    }
48}
49
50/* output:
51 * ext f()
52 * g()
53 * ext p()
54 * ----
55 * ext f()
56 * g()
57 * ----
58 * f()
59 * g()
60 */

基类和导出类的p方法,看上去像是导出类覆盖了基类的方法,实际上这是一种“字面”的覆盖,因为private方法并不是基类接口的一部分,它和导出类的p方法只是具有相同名称而已。

还需要注意的是,即使使用了向上转型试图调用基类的方法,实际上却失败了,因为子类覆盖了这个方法。这种行为在Java中被称为动态绑定,即编译器知道实际引用的对象类型,从而去调用对应的方法

final类--阻止继承 #

如果将一个类声明为final的(通常使用final class ...),意味着这个类不能被继承。

也就是说,该类的所有方法都含有一个隐式的final修饰符。

final类的域可以是或不是final,规则同 final数据一致。


参考: https://docs.oracle.com/javase/specs/jls/se18/html/jls-17.html#jls-17.5