final
在英文中是“最终的”、“不可改变的”意思。在 Java 中,它正是用来表示这种“不可变性”的语义。它可以用来修饰变量、方法和类,但在这三种上下文中的具体含义和效果有所不同。
1. final
修饰变量
这是 final
最常用的情况。一个被 final
修饰的变量只能被赋值一次,一旦初始化后,其值就不能再被更改。本质上,它变成了一个常量。
根据变量的类型,我们又可以分为:基本类型变量和引用类型变量。
a) final
修饰基本类型变量
变量的值不能再改变。
final int maxScore = 100;
// maxScore = 95; // 编译错误!无法为最终变量maxScore再次赋值
final double pi = 3.14159;
// pi = 3.14; // 编译错误!
b) final
修饰引用类型变量
变量的引用(即指向内存地址的“指针”)不能再改变。但是,它所指向的那个对象内部的状态(对象的属性)是可以被修改的。
这一点非常重要,也常常是初学者混淆的地方。
// 定义一个简单的Person类
class Person {
public String name;
public int age;
}
public class Test {
public static void main(String[] args) {
final Person person = new Person(); // person是一个final引用
person.name = "Alice"; // 正确:可以修改对象内部的属性
person.age = 25; // 正确:可以修改对象内部的属性
// person = new Person(); // 编译错误!无法将新对象的引用赋给最终变量person
System.out.println(person.name); // 输出: Alice
}
}
关键理解:final Person person
意味着 person
这个“遥控器”只能控制最开始绑定的那个“电视”,不能再绑定到别的“电视”上。但是,用这个“遥控器”去调台、调音量(修改对象的属性)是完全允许的。
final
变量的初始化
final
变量必须在明确初始化之后才能使用。有两种初始化方式:
-
声明时直接初始化:
final int CONSTANT_VALUE = 10;
-
在构造器或构造代码块中初始化(对于实例变量):
这种方式非常有用,它允许一个类的每个实例拥有不同的“final”值。class MyClass { final int instanceFinalVar; public MyClass(int value) { this.instanceFinalVar = value; // 在构造器中初始化 } // 或者使用构造代码块 // { // instanceFinalVar = 100; // } } public class Test { public static void main(String[] args) { MyClass obj1 = new MyClass(10); MyClass obj2 = new MyClass(20); // obj1和obj2的instanceFinalVar值不同,但各自初始化后都不能再改变。 } }
静态 final
变量(常量)
如果 final
和 static
一起修饰一个变量,那它就成为了一个类常量。通常用全大写字母和下划线命名,必须在声明时或静态代码块中初始化。
class Constants {
public static final String APPLICATION_NAME = "MyApp";
public static final double PI;
static {
PI = 3.1415926; // 在静态代码块中初始化
}
}
2. final
修饰方法
当一个方法被 final
修饰时,表示这个方法不能被子类重写(Override)。
设计目的:
- 防止继承体系中的关键方法被篡改:确保父类中的某个方法行为在子类中保持不变。
- 效率考虑(早期Java):编译器在早期可能会对
final
方法进行内联优化。不过现代 JVM 已经非常智能,这个优化意义不大了。
class Parent {
// 这是一个不允许子类重写的方法
public final void forbiddenToOverride() {
System.out.println("这是父类的最终实现,子类不能修改。");
}
public void canBeOverridden() {
System.out.println("子类可以重写我。");
}
}
class Child extends Parent {
// @Override
// public void forbiddenToOverride() { } // 编译错误!无法重写父类中的final方法
@Override
public void canBeOverridden() { // 正确:可以重写非final方法
System.out.println("子类重写了这个方法。");
}
}
3. final
修饰类
当一个类被 final
修饰时,表示这个类不能被继承,即不能有子类。
设计目的:
- 保证类的行为和安全:如果一个类的实现已经非常完美,或者从安全角度考虑,不希望任何人通过继承来修改其行为(例如,可能会引入恶意代码),就将其声明为
final
。 - JDK 中的例子:
String
,Integer
,Double
等包装类都是final
类。这保证了像String
的不可变性等核心特性不会被破坏。
final class FinalClass {
public void someMethod() {
// ...
}
}
// class SubClass extends FinalClass { } // 编译错误!无法从final类FinalClass继承
总结与最佳实践
修饰目标 | 含义与效果 | 常见场景 |
---|---|---|
变量 | 只能赋值一次,成为常量。 | 1. 定义程序中的字面常量(如 MAX_SIZE )。2. 确保引用在初始化后不再指向其他对象。 |
方法 | 不能被子类重写。 | 1. 保护关键方法的实现逻辑。 2. 在涉及继承的严谨架构设计中明确禁止重写。 |
类 | 不能被继承。 | 1. 设计不可变类(如 String )。2. 保证核心 API 的安全和稳定。 |
最佳实践与深入理解:
-
final
与不可变对象:
要创建一个真正的不可变对象,需要满足:- 类声明为
final
(或者所有构造方法都是private
的,并用静态工厂方法创建对象),防止子类改变其行为。 - 所有字段用
private final
修饰。 - 不提供修改内部状态的
setter
方法。 - 如果字段是引用类型,要确保不会泄露该引用,或者在返回时返回其深拷贝。
- 类声明为
-
final
与线程安全:
正确初始化的final
字段具有很好的线程安全性。JVM 内存模型保证,在构造方法中初始化一个final
字段时,只要对象被正确构造(没有发生this
引用逸出),那么其他线程就能看到最终初始化后的值,无需额外的同步。这也是为什么不可变对象天生是线程安全的。 -
final
参数:
在方法参数列表中也可以使用final
,表示在方法内部不能修改这个参数的值(对于基本类型)或引用(对于引用类型)。这主要是一种代码规范和设计意图的体现,可以提高代码的可读性,并防止无意中的修改。public void process(final String message, final int id) { // message = "new message"; // 编译错误! // id = 456; // 编译错误! System.out.println("Processing " + message + " with ID: " + id); }