一、Java 中 final
关键字有哪些作用?它可以用在哪些地方?
1. final
修饰变量
- 修饰基本数据类型:一旦赋值后,值就不能再被修改。
- 修饰引用类型:引用的地址不能变,但对象的内容可以修改。
示例:
final int a = 10;
a = 20; // ❌ 编译错误,不能修改
final StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // ✅ 可以修改内容
sb = new StringBuilder("New"); // ❌ 不能修改引用地址
2. final
修饰方法
- 防止方法被子类重写,但子类仍然可以继承该方法(只能使用,不能修改)。
示例:
class Parent {
final void show() {
System.out.println("This is a final method.");
}
}
class Child extends Parent {
void show() { // ❌ 编译错误,不能重写 final 方法
System.out.println("Overriding...");
}
}
3. final
修饰类
- 防止类被继承,即该类不能有子类。
示例:
final class Animal {
void sound() {
System.out.println("Some sound");
}
}
// ❌ 编译错误,不能继承 final 类
class Dog extends Animal {
}
4. final
结合 static
(常量定义)
通常用于定义不可变的全局常量:
public class Constants {
public static final double PI = 3.14159;
}
二、追问:final
修饰的方法一定会被 inline
(内联优化)吗?
内联优化(Inlining Optimization) 是一种 方法调用优化技术,它的核心思想是:
👉 用被调用方法的代码直接替换方法调用,避免方法调用的开销,提高运行效率。
final
修饰的方法不一定会被 inline(内联优化),尽管它有助于编译器或 JIT(Just-In-Time)编译器进行内联优化,但是否真正执行 inline 取决于多个因素。
1. 为什么 final
方法可能会被内联?
final
方法不能被子类重写,这意味着其行为在编译期和运行期都是确定的,JVM 可以更安全地优化。- 编译器或 JIT 编译器(即时编译器)可以确定方法的调用目标,从而减少虚方法表(vtable)查找的开销,优化方法调用。
2. final
方法一定会被内联吗?
**不一定!**JVM 是否内联一个方法,取决于多个因素,例如:
(1)方法体的大小
- JVM 主要基于方法体大小来决定是否内联,如果方法 过长,即使是
final
方法,JVM 也可能不会进行内联。 - HotSpot JVM 默认的 方法内联大小阈值 大约是 35 个字节的字节码(可通过
-XX:MaxInlineSize
调整)。
(2)JVM 内联策略
JVM 主要通过两种方式进行方法内联:
- 静态内联(Static Inlining)
- 发生在 JIT 编译阶段,即字节码被编译为机器码时。
final
方法、private
方法、static
方法以及某些virtual
方法可能被内联。
- 动态内联(Dynamic Inlining)
- JVM 基于运行时的热点信息决定是否进行内联,通常调用频率高的方法才会被内联。
- HotSpot JVM 有一个 方法调用计数器,当方法的调用次数超过
-XX:FreqInlineSize
阈值(默认 325 次),才会被 JIT 编译器考虑内联。
(3)是否启用了 JIT(即时编译器)
- 只有在 JIT 编译(如 C1 编译器或 C2 编译器)启用时,才会进行方法内联。
- 如果运行在解释模式(如
-Xint
选项),则不会进行 JIT 编译,也不会进行内联。
(4)内联受限于其他 JVM 选项
-XX:+PrintCompilation
:可以查看 JIT 编译过程,检查方法是否被内联。-XX:+Inline
:强制 JVM 尝试进行内联。-XX:MaxInlineSize=xxx
:调整允许内联的方法大小。
3. 示例:查看 final
方法是否被内联
代码示例
public class InlineTest {
public static final int iterations = 1000000;
public static void main(String[] args) {
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
compute();
}
long end = System.nanoTime();
System.out.println("Time: " + (end - start) / 1_000_000 + " ms");
}
public static final void compute() {
int x = 10;
int y = 20;
int z = x + y;
}
}
使用 JVM 选项查看是否内联
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+LogCompilation InlineTest
如果 compute()
方法被内联,日志可能会显示:
100 4 InlineTest::compute (8 bytes) inline (hot)
这表明 compute()
方法被成功内联。
三、final 修饰的变量一定是编译期常量吗?
在 Java 中,使用 final 修饰变量表示该变量在初始化后其值不能再改变,但这并不意味着它一定是编译期常量。是否是编译期常量还取决于变量的声明和初始化方式,主要有以下几点区别:
-
编译期常量的条件
- 变量必须是基本数据类型或 String 类型。
- 必须在声明时直接使用字面量或编译期可确定的表达式赋值。
- 通常还会同时用上 static,例如:
public static final int MAX_COUNT = 100; public static final String MESSAGE = "Hello";
这种情况下,编译器在编译时就能确定其值,并在使用该常量的地方直接内联。
-
final 变量不一定是编译期常量
- 如果 final 变量的值是在运行时才能确定的(例如通过方法调用、计算或随机数生成),则它虽然在初始化后不可修改,但并非编译期常量。例如:
这里,runtimeValue 的值只有在运行时才能确定。public final int runtimeValue = new Random().nextInt();
- 对于非 static 的 final 成员变量,即使用常量表达式初始化,它们也属于对象的实例数据,不能在编译时直接内联到调用处。
- 如果 final 变量的值是在运行时才能确定的(例如通过方法调用、计算或随机数生成),则它虽然在初始化后不可修改,但并非编译期常量。例如:
-
总结
- final 修饰的变量只保证一旦赋值后不可改变,并不保证在编译期间就能确定其值。
- 只有满足基本数据类型或 String 类型、在声明时直接使用常量表达式赋值、并且通常加上 static 修饰的 final 变量,才被视为编译期常量,可以在编译期间进行内联优化。
因此,final 修饰的变量不一定都是编译期常量,只有符合上述条件的 final 变量才能被认为是编译期常量。
四、在多线程环境中,final 变量有什么作用?
在多线程环境中,使用 final 修饰的变量主要起到以下几个作用:
-
保证不可变性和线程安全性
- 对于基本数据类型或者 String 这种不可变类型来说,final 修饰后,变量在初始化后就不能再被修改,因此多个线程同时读取它时不会引发数据不一致的问题。
- 如果一个对象的所有状态都是不可变的(例如所有成员都被声明为 final 且在构造函数中正确赋值),那么这个对象就称为不可变对象,而不可变对象本身就是线程安全的,发布后无需额外同步就能被安全共享。
-
安全发布的保证
- Java 内存模型对 final 域有特殊的“安全发布”语义:
当一个对象在构造函数中对其 final 域进行赋值后,JVM 会保证在该对象的引用对其他线程可见时,这些 final 域的值已经正确初始化,不会出现“部分构造”或重排序问题。这意味着,即使没有使用显式的同步机制,只要对象在构造期间没有逸出(即 this 没有在构造函数中被传出),其他线程在看到该对象时就能看到 final 域的正确值 - 这种机制可以防止由于指令重排序导致的线程安全问题,确保在对象发布时各个 final 域已完全初始化,从而提高了多线程程序的安全性。
- Java 内存模型对 final 域有特殊的“安全发布”语义:
-
减少同步开销和提升性能
- 由于 final 变量在初始化后不再改变,编译器和 JVM 可以对它们进行优化(例如常量折叠或内联),从而减少对内存的重复读取。
- 同时,由于不可变状态天然线程安全,使用 final 修饰的变量可以避免额外的同步操作,降低锁竞争带来的性能损耗
-
明确设计意图
- 在代码中使用 final 修饰变量可以清晰地表达“此变量在初始化后不应被改变”的设计意图,这不仅有助于维护者理解代码逻辑,同时也降低了因误修改而导致的线程安全风险。
总结
在多线程环境下,final 变量通过确保变量值不可变和提供安全发布语义,使得对象在构造完成后能够被其他线程正确地读取,无需额外的同步措施,从而简化了并发编程的复杂性并提高了性能。
这些作用使得在编写并发程序时,尽量将那些不需要修改的变量声明为 final 成为一种良好的编程习惯。