final关键字详解

# final关键字详解

# 1、final普通语义

# ①、final修饰变量

不可变性:
当一个变量被声明为final时,意味着它的值一旦被初始化就不能再改变。对于基本数据类型,这意味着该值不能更改;
对于引用类型,这意味着该引用不能指向另一个对象(但对象的内容可能仍然可以修改,除非该对象也是final)。

编译器优化:
由于final变量的值在编译时就可以确定,编译器可能会进行一些优化,比如将对final变量的访问替换为常量值,以提高程序执行效率。 比如我在Java基础知识点 (opens new window)这篇文章中有提到final的编译器优化的细节。

# ②、final修饰方法

不可重写:
如果一个方法被声明为final,那么在任何继承自包含该方法的类的子类中,都不能重写(override)这个方法。这可以防止子类无意中改变父类的行为。

注意点:
final修饰的方法 可以被重载。

# ③、final修饰类

不可继承:
如果一个类被声明为final,那么这个类不能被继承。也就是说,其他类不能声明为这个final类的子类。这是实现封装的一种方式,可以确保类的设计不被外部修改。最常被提到的应该就是 String类了。

# 补充点:

# 如果想扩展String类该怎么做?引出组合设计模式

Java中最直接的扩展就是利用继承,但是 String类 是final修饰的无法被继承。
我在ArrayDeque详解 (opens new window)这篇文章有提到一点,利用ArrayDeque实现栈。
实际上我们可以参考这种模式来扩展String。

我们可以利用String来实现自己的MyString:

public class TestA {

    public static void main(String[] args) {

        MyString myString = new MyString("666");
        int length = myString.length();
        System.out.println(length);   // 3

        MyString str = new MyString("  ");   
        System.out.println(str.isNotEmpty());  // true
        System.out.println(str.isNotBlank()); // false
    }
}


class MyString {
    private String str;

    // 构造函数
    public MyString(String str) {
        this.str = str;
    }

    // 添加自定义功能 判断字符串不为空
    public boolean isNotEmpty() {
        return str != null && str.length() != 0;
    }

    // 添加自定义功能 判断字符串不为空 且不为空格
    public boolean isNotBlank() {
        int strLen;
        if (str == null || (strLen = str.length()) == 0) {
            return false;
        }
        for (int i = 0; i < strLen; i++) {
            if ((!Character.isWhitespace(str.charAt(i)))) {
                return true;
            }
        }
        return false;
    }

    // 其他标准String类的方法可以直接调用
    public int length() {
        return str.length();
    }

    // 覆盖toString方法
    @Override
    public String toString() {
        return "这是我自定义的String类的toString方法:" + str;
    }
}

可以看到上面代码添加了isNotBlank()isNotEmpty()方法,相当于扩展了String的功能,同时如果想使用String原有的功能,就把原有的方法搬过来即可。例如上面的length()方法。

那么这种扩展类的方式有个正式名称叫组合模式。它是结构型设计模式中的一种。
组合设计模式指的是在一个类中包含另一个类的实例,从而扩展其功能,而不是通过继承来扩展类的功能。
这样可以更好地遵循面向对象设计原则,特别是“组合优于继承”(Composition over Inheritance)原则。

在上面的例子中,MyString 类包含了一个 String 对象,并通过这个对象提供了原始 String 类的功能,比如length(),同时还添加了一些自定义的功能。这就是组合设计模式的一个典型应用。

组合设计模式的优点
灵活性: 组合使类可以在运行时动态改变其行为,因为它们可以包含不同的对象实例。
复用性: 通过组合,可以更好地复用代码,而不需要重复定义相同的功能。
避免继承的局限性: 继承在某些情况下可能会导致类层次结构变得复杂且难以维护,而组合则可以保持类的层次结构简单,而且有些类不能够被继承,例如String。

组合 vs 继承
继承: 是一种“is-a”关系,例如“猫是动物”,即 Cat extends Animal。
组合: 是一种“has-a”关系,例如“车有引擎”,即 Car has an Engine。

在上面的例子中,MyString 是通过组合拥有一个 String 对象,因此可以说 MyString 有一个 String 对象,而不是 MyString 是一个 String 对象。这种设计使得 MyString 类可以灵活地扩展和修改,而不影响 String 类的实现。

# 匿名内部类 访问的数据 为什么需要被申明为 final?

参考我的另一篇文章Java基础知识点 (opens new window)第42点。

# 2、final的内存语义

在Java的并发编程中,final关键字不仅用于定义常量、方法、类和变量的不可变性,它还具有重要的内存语义,可以确保对象的安全发布。

# ①、 构造函数内赋值

当一个final字段在对象的构造函数中被赋值后,并且构造函数本身没有将this引用逸出(即在构造对象期间没有泄漏this引用),则所有对这个final字段的读操作都会看到这个值,且不会发生重排序。这意味着,一旦构造函数完成,其他线程可以立即看到final字段的正确值。

什么是this逃逸? This逃逸(This Escape)是指在对象的构造过程中,this引用被暴露到构造函数外部的情况。这种暴露可能会导致未完全构造的对象被其他线程访问,从而引发不确定的行为和潜在的并发问题。

class MyClass {

    final int a;

    MyClass obj;

    public MyClass() {
        a = 10;
        obj = this;
    }
    
}

# ②、禁止重排序

JVM和处理器禁止将final字段的赋值操作与构造函数外的代码重排序。 即 final字段的写入和随后对这个对象的引用(即构造函数的结束)不会被重排序。这确保了final字段在构造函数完成后是可见的。

处理器层面:
对写final变量的重排序规则会要求编译器在final的写之后,构造函数return之前插入一个StoreStore障屏。读final变量的重排序规则要求编译器在读final的操作前面插入一个LoadLoad屏障。

Java中volatile关键字详解 (opens new window)这篇文章中说过,x86架构处理器的指令重排序规则仅会对写-读操作做重排序,所以x86架构处理器在处理final读写时,不需要加内存屏障。

总结:
JSR-133通过增强Java内存模型中的final语义,确保了在正确构造对象的情况下,多线程环境中的初始化安全性。这个改进是随着JDK 5一起引入的,并在后续版本中得到了持续的支持和优化。通过这些改进,程序员可以更加安全地在多线程环境中使用final,而无需额外的同步机制。

public class TestA {
    final int x;
    static TestA obj;

    public TestA() {
        x = 3; // final的写操作
    }

    public static void writer() {
        obj = new TestA(); // 赋值操作发生在构造函数之后
    }

    public static void reader() {
        if (obj != null) {
            int temp = obj.x; // 读取final
            System.out.println(temp);
        }
    }

    public static void main(String[] args) {
        // 多线程模拟
        new Thread(TestA::writer).start();
        new Thread(TestA::reader).start();
    }
}