【Java基础】Java 中 的`final` 关键字
前言
在 Java 编程的世界里,final 关键字是一个强大且常用的工具。它可以应用于类、方法和变量,赋予它们不同的 “不可变” 特性。
一、final
修饰类
1. 定义与特性
当使用 final
修饰一个类时,这个类就如同被上了一把坚固的锁,不能被其他类继承,即不会有子类。这是 Java 语言精心设计的一种机制,目的是确保类的设计和实现不会被意外修改,从而保证类的行为和功能始终保持一致。
2. 示例代码
// 定义一个 final 类
final class FinalClass {
public void printMessage() {
System.out.println("This is a final class.");
}
}
// 以下代码会编译错误,因为 FinalClass 是 final 类,不能被继承
// class SubClass extends FinalClass {
// }
public class Main {
public static void main(String[] args) {
FinalClass fc = new FinalClass();
fc.printMessage();
}
}
3. 代码解释
在上述代码中,FinalClass
被明确声明为 final
类。当我们试图创建它的子类 SubClass
时,编译器会立刻抛出错误,这是因为 Java 严格禁止继承 final
类。这种限制就像是给类加上了一层保护罩,确保其内部的实现不会被外部通过继承的方式随意改变。
4. 应用场景
- 安全性:在 Java 的核心类库中,有许多重要的类被设计成
final
类,例如java.lang.String
。这是因为字符串在 Java 里是广泛使用的基础数据类型,为了防止用户通过继承来修改其行为,保证字符串操作的安全性和一致性,将其设计为final
类是非常必要的。 - 性能优化:由于
final
类不能被继承,编译器在处理这类类时可以进行更多的优化。例如,在调用final
类的方法时,编译器能够直接确定方法的具体实现,避免了动态绑定所带来的开销,从而显著提高代码的执行效率。
二、final
修饰方法
1. 定义与特性
当 final
修饰一个方法时,这个方法就像是被贴上了“禁止修改”的标签,不能被其子类重写(覆盖)。这一特性确保了方法的实现逻辑在整个继承体系中始终保持不变,保证了方法行为的一致性和稳定性。
2. 示例代码
class ParentClass {
// 定义一个 final 方法
public final void finalMethod() {
System.out.println("This is a final method.");
}
public void nonFinalMethod() {
System.out.println("This is a non-final method.");
}
}
class ChildClass extends ParentClass {
// 以下代码会编译错误,因为 finalMethod 是 final 方法,不能被重写
// @Override
// public void finalMethod() {
// System.out.println("Trying to override final method.");
// }
@Override
public void nonFinalMethod() {
System.out.println("Overriding non-final method.");
}
}
public class Main {
public static void main(String[] args) {
ChildClass cc = new ChildClass();
cc.finalMethod();
cc.nonFinalMethod();
}
}
3. 代码解释
在上述代码中,ParentClass
中的 finalMethod
被声明为 final
方法。当 ChildClass
尝试重写该方法时,编译器会报错,这清晰地体现了 final
方法的不可重写特性。而 nonFinalMethod
没有被 final
修饰,所以可以在 ChildClass
中被重写,以满足不同的业务需求。
4. 应用场景
- 保证方法实现的一致性:当一个方法的实现逻辑是核心且不希望被子类改变时,我们可以将其声明为
final
方法。例如,Object
类中的getClass()
方法就是final
方法,它确保了在任何情况下获取对象类信息的方式都是一致的,不会因为子类的重写而产生混乱。 - 性能优化:
final
方法在调用时可以避免动态绑定的过程,因为编译器在编译时就能够确定方法的具体实现。这样一来,方法调用的效率得到了显著提高,尤其是在频繁调用的场景下,性能提升更为明显。
三、final
修饰变量
1. 基本数据类型变量
对于基本数据类型的变量,一旦使用 final
修饰,它就摇身一变成为了常量。这个常量的值一旦被赋值,就如同被封印一般,不能再被改变。并且,final
修饰的变量必须要有初始值,这是 Java 语言的硬性规定。因为一旦变量被声明为 final
,它就失去了重新赋值的资格,如果没有初始值,就会导致变量永远处于未赋值的状态,这在 Java 中是不被允许的。
示例代码
public class Main {
public static void main(String[] args) {
// 正确,给 final 变量赋初始值
final int num = 10;
// 以下代码会编译错误,因为 num 是 final 变量,不能被重新赋值
// num = 20;
System.out.println("The value of num is: " + num);
// 以下代码会编译错误,因为 final 变量必须有初始值
// final int uninitialized;
// uninitialized = 30;
}
}
代码解释
在上述代码中,num
被声明为 final
变量并赋予初始值 10。此后,任何试图重新赋值的操作都会引发编译错误。同时,如果声明一个 final
变量但没有立即赋值,后续再进行赋值同样会报错,这再次强调了 final
变量必须在声明时就确定初始值。
2. 引用数据类型变量
对于引用数据类型的变量,final
修饰的效果稍有不同。当一个引用类型的变量被 final
修饰后,它所引用的对象就像被固定在了一个位置,不能再被改变指向其他对象。然而,对象的内容是可以被修改的。同样,final
修饰的引用类型变量也必须在声明时或构造函数中进行初始化。
示例代码
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
// 声明并初始化一个 final 引用类型变量
final List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
System.out.println("List elements: " + list);
// 以下代码会编译错误,因为 list 是 final 变量,不能被重新赋值
// list = new ArrayList<>();
}
}
代码解释
在上述代码中,list
被声明为 final
变量,它引用了一个 ArrayList
对象。我们可以自由地向这个 ArrayList
中添加或删除元素,也就是改变对象的内容。但是,一旦 list
引用了某个 ArrayList
对象,就不能再让它引用另一个 ArrayList
对象,否则会引发编译错误。
3. 静态常量(private static final
)
private static final
组合常常被用于定义类的静态常量。其中,private
关键字的作用是将该常量的访问权限限制在类的内部,确保其不会被外部随意访问和修改;static
表示该常量属于类本身,而不是类的某个实例,这意味着无论创建多少个类的实例,该常量只有一份副本;final
则保证了该常量的值一旦确定,就不能再被改变。这种常量在编译时就已经被确定,并且在整个程序的运行过程中始终保持不变。
示例代码
public class Constants {
private static final int MAX_VALUE = 100;
public static void main(String[] args) {
System.out.println("The maximum value is: " + MAX_VALUE);
}
}
代码解释
在上述代码中,MAX_VALUE
是一个使用 private static final
修饰的静态常量。它只能在 Constants
类的内部被访问,并且其值不能被修改。这种方式常用于定义一些全局的、不可变的常量,能够大大提高代码的可读性和可维护性。
4. final
变量在不同位置的初始化
- 成员变量:
final
成员变量的初始化方式比较灵活,既可以在声明时直接进行初始化,也可以在构造函数中完成初始化。但需要特别注意的是,必须保证在每个构造函数中都对其进行初始化,否则会引发编译错误。
class FinalMemberVariable {
// 声明时初始化
final int num1 = 10;
final int num2;
// 构造函数中初始化
public FinalMemberVariable() {
num2 = 20;
}
}
- 局部变量:
final
局部变量的初始化规则相对简单,它必须在使用前进行初始化,一旦完成初始化,就不能再对其进行重新赋值。
public class FinalLocalVariable {
public static void main(String[] args) {
final int num;
num = 30; // 初始化
// num = 40; // 编译错误,不能重新赋值
System.out.println(num);
}
}
5. final
变量和不可变对象的区别
虽然 final
修饰引用类型变量时,引用不可变,但对象内容可变,而不可变对象是指对象一旦创建,其内部状态就不能被改变。例如,String
类就是不可变对象,即使不使用 final
修饰,其内容也不能被修改。
String str = "Hello";
// 下面这行代码实际上是创建了一个新的 String 对象,而不是修改原对象
str = str + " World";
与之对比,使用 final
修饰的 List
,虽然引用不能变,但可以修改列表内容。
final List<String> finalList = new ArrayList<>();
finalList.add("Element"); // 可以修改列表内容
四、final
修饰的常量在编译时的替换原理
1. 原理概述
当 final
修饰的常量是基本数据类型或字符串常量时,在编译阶段,编译器会如同一个智能的替换机器,将代码中对该常量的引用直接替换为常量的值。这是因为编译器在编译时就已经确切地知道了常量的值,为了提高代码的执行性能,它会直接将常量的值嵌入到代码中,从而避免了在运行时对常量的额外访问。
2. 示例代码及编译后分析
编写的代码
public class CompileTimeReplacement {
private static final int NUM = 10;
public static void main(String[] args) {
int result = NUM * 2;
System.out.println("The result is: " + result);
}
}
编译后的代码(伪代码表示)
public class CompileTimeReplacement {
public static void main(String[] args) {
int result = 10 * 2; // 编译器直接将 NUM 替换为 10
System.out.println("The result is: " + result);
}
}
代码解释
在上述代码中,NUM
是一个 final
常量。在编译过程中,编译器会将 result = NUM * 2;
这行代码中的 NUM
直接替换为其值 10,得到 result = 10 * 2;
。这样一来,在运行时就不需要再去访问 NUM
这个常量,直接进行计算即可,大大提高了代码的执行效率。
3. 注意事项
需要注意的是,只有当 final
常量的值在编译时就能够确定的情况下,才会进行替换操作。如果 final
常量的值是在运行时才能确定的,那么编译器就无法进行替换。
import java.util.Random;
public class NonCompileTimeReplacement {
private static final int NUM;
static {
Random random = new Random();
NUM = random.nextInt(100); // 运行时确定值
}
public static void main(String[] args) {
int result = NUM * 2;
System.out.println("The result is: " + result);
}
}
在这个例子中,NUM
的值是在运行时通过 Random
类随机生成的,编译器在编译时无法确定其具体值,所以不会对代码中的 NUM
进行替换操作。
4. 对代码维护的影响
编译时替换虽然提高了性能,但也会对代码维护产生一定影响。如果常量的值需要修改,仅仅修改常量的定义是不够的,因为已经编译的代码中常量引用已经被替换。这就需要重新编译所有引用该常量的代码,否则可能会出现不一致的情况。
五、final
与多线程
1. 线程安全性
final
变量在多线程环境下展现出了良好的线程安全性。由于 final
变量一旦完成初始化,其值就不能再被修改,所以多个线程可以安全地访问 final
变量,而无需额外的同步机制来保证数据的一致性。
2. 示例代码
public class FinalInMultiThreading {
private final int num;
public FinalInMultiThreading(int num) {
this.num = num;
}
public int getNum() {
return num;
}
public static void main(String[] args) {
FinalInMultiThreading obj = new FinalInMultiThreading(10);
// 线程 1 访问 num
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1: " + obj.getNum());
});
// 线程 2 访问 num
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2: " + obj.getNum());
});
thread1.start();
thread2.start();
}
}
3. 代码解释
在上述代码中,num
是一个 final
成员变量。多个线程可以同时访问 obj.getNum()
方法,而不会出现数据不一致的问题。这是因为 num
的值一旦在构造函数中被初始化,就不会再被改变,各个线程读取到的始终是同一个稳定的值。
4. 与 volatile
关键字的对比
volatile
关键字主要用于保证变量的可见性,即一个线程修改了 volatile
变量的值,其他线程能立即看到最新的值。而 final
关键字主要用于保证变量的不可变性。在多线程场景中,如果一个变量既需要保证不可变,又需要保证可见性,那么可以结合使用 final
和 volatile
关键字。
class VolatileFinalExample {
private final int finalValue;
private volatile boolean isInitialized;
public VolatileFinalExample(int value) {
this.finalValue = value;
this.isInitialized = true;
}
public int getFinalValue() {
if (isInitialized) {
return finalValue;
}
return -1;
}
}
总结
final
关键字在 Java 中无疑是一个功能强大且用途广泛的工具,它可以灵活地修饰类、方法和变量,赋予它们不同的“不可变”特性。
final
类不能被继承,这为类的安全性和一致性提供了坚实的保障,同时也为编译器进行性能优化创造了有利条件。final
方法不能被重写,确保了方法实现的稳定性,避免了子类意外修改方法逻辑,显著提高了方法调用的效率。final
变量一旦赋值就不能再被重新赋值,对于基本数据类型而言是值不可变,对于引用数据类型则是引用不可变,但对象内容可变。而且,final
变量必须有初始值,并且在不同的位置有不同的初始化要求。private static final
组合常用于定义类的静态常量,这种方式能够极大地提高代码的可读性和可维护性。final
修饰的常量在编译时,如果其值在编译时就能确定,会被直接替换为常量的值,从而有效地提高了代码的执行效率,但也会对代码维护产生一定影响。- 在多线程环境下,
final
变量具有一定的线程安全性,多个线程可以安全地访问final
变量,无需担心数据不一致的问题。同时,可结合volatile
关键字满足更多场景需求