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