Java学习笔记之深入理解关键字final

基本概念

final是Java中是一个保留的关键字,可以声明变量(包括成员变量[实例变量和静态变量]和局部变量)、方法和类。它通常指“这是无法改变的”。将变量、方法和类声明为final,JVM能够对其进行优化,进而提升性能。
其关键知识点如下:

  • final方法是在编译器绑定的,即静态绑定(可参考Java学习笔记之深入理解动态绑定和静态绑定)
  • final成员变量必须在声明的时候初始化或者在构造器中初始化
  • final局部变量必须在使用前进行初始化赋值
  • 当final变量是基本数据类型时,其数值恒定不变,且不能被更改;当final变量是引用类型时,其引用恒定不变,即不能再指向其它对象,但其引用指向的对象本身却是可以被修改的
  • final方法不能被重写
  • final类不能被继承
  • 类中所有的private方法都隐式地指定为final

final变量

无论是成员变量(包括实例变量和静态变量)还是局部变量,只要声明为final,就必须在使用前进行初始化,否则在编译期时出现错误。值得注意的是,成员变量如果没有声明为final,那么在使用前是可以不用进行初始化的,因为它们有默认值。(可参考Java学习笔记之变量类型)
例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.ArrayList;

public class FinalVarDemo {
int var; // It's OK!默认值为0
//final int var_final; // It's not OK! 必须在声明的时候初始化或者在构造器中初始化
final int var_instance = 0;
final ArrayList<String> arrayList = new ArrayList<>();

void test(){
//final int var_local; // It's not OK!局部变量在使用前必须进行初始化赋值
final int var_local = 1;
//var_local = 2; // It's not OK!final局部变量值不能更改
//var_instance = 1; // It's not OK!final实例变量值不能被更改
//arrayList = new ArrayList<>(); // It's not OK!不能将final引用类型变量指向新的对象
arrayList.add("hello"); // It's OK!可以改变对象本身
arrayList.add("world");

System.out.println("var = "+var);
System.out.println("var_instance = "+var_instance);
System.out.println("var_local = "+var_local);
System.out.println("arrayList = "+arrayList);
}

public static void main(String[] srgs){
FinalVarDemo finalVarDemo = new FinalVarDemo();
finalVarDemo.test();
}
}

输出结果:

1
2
3
4
var = 0
var_instance = 0
var_local = 1
arrayList = [hello, world]

final参数

Java允许在参数列表中以声明的方法将参数指明为final。
例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Dog{
void bark(){}
}
public class FinalArg {
void withFinal(final Dog dog){
//dog = new Dog(); // Illegal! --dog is final
}

void withoutFinal(Dog dog){
dog = new Dog(); // OK! --dog is not final
}

int f(final int i){
//i++; // Illegal! --i is final,can't change
return i + 1;
}

int g(int i){
i++; // OK! --i is not final
return i;
}
}

分析:将方法中的参数指定为final,意味着无法在方法中更改参数引用所指向的对象,如果是基本数据类型参数,不能改变其值,只能读取。这一特性,一般用来向匿名内部类传递数据。

final方法

方法前面加上final关键字,代表这个方法不可以被子类的方法重写。final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。
例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Animal{
private final void f(){
System.out.println("Animal.f()");
}
private void g(){
System.out.println("Animal.g()");
}
}

class Dog extends Animal{
public final void f(){
System.out.println("Dog.f()");
}
public void g(){
System.out.println("Dog.g()");
}
}

class Teddy extends Dog{
// It's not OK!final方法不能被覆盖
// public final void f(){
// System.out.println("Teddy.f()");
// }
public void g(){
System.out.println("Teddy.g()");
}
}

public class FinalMethodDemo {
public static void main(String[] args){
Teddy teddy = new Teddy();
teddy.f();
teddy.g();
Dog teddy1 = new Teddy();
teddy1.f();
teddy1.g();
Animal teddy2 = new Teddy();
// teddy2.f(); // It's not OK!final方法不能被覆盖
// teddy2.g();
}
}

输出结果:

1
2
3
4
Dog.f()
Teddy.g()
Dog.f()
Teddy.g()

结果分析:需要指明的是类中所有的private方法都隐式的指定为final。而final方法是不能被覆盖的。但从例3中的代码可以看出,覆盖一个private方法(隐含是final的)似乎是奏效的。类Dog继承于类Animal,并且覆盖了两个private方法,而且编译也没有出错。那么问题来了,谁说final方法是不能被覆盖的?一般来讲,重写方法使用的是动态绑定,如果类Dog真的是重写了类Animal中的两个private方法,那么输出应该是Animal.f()而不是Dog.f(),所以可以肯定的是,类Dog中的这两个方法并不是继承自类Animal,它只不过是具有相同的名称,仅此而已!当然,这说的是private方法,对于public final方法,如果你想要覆盖该方法,编译会出现错误提示,该方法不能被覆盖,正如例3中的代码写的一样。

final类

当将某个类定义为final时,表示该类不能被继承。final类通常功能是完整的。Java中有许多这样的类,如String,Interger以及其他包装类。

1
2
3
4
5
6
7
8
final class Animal{

}

//compilation error: cannot inherit from final class
class Dog extends Animal{

}

final, finally, finalize的区别

  • final 用于声明属性,方法和类, 分别表示属性不可变, 方法不可覆盖, 类不可继承
  • finally 是异常处理语句结构的一部分,表示总是执行
  • finalize 是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等. JVM不保证此方法总被调用

面试中的相关问题

1.当try和catch中有return时,finally将会怎样执行?其执行顺序是怎样的?
先来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Test {
public static void main(String[] args) {
System.out.println(beforeFinally());
}

public static int beforeFinally(){
int a = 0;
try{
a = 1;
return a;
}finally{
a = 2;
}
}
}
/**output:
1
*/

从结果上看,貌似finally里的语句是在return之后执行的,其实不然,实际上finally里的语句是在在return之前执行的。那么问题来了,既然是在之前执行,那为什么a的值没有被覆盖了?
实际过程是这样的:当程序执行到try{}语句中的return方法时,它会干这么一件事,将要返回的结果存储到一个临时栈中,然后程序不会立即返回,而是去执行finally{}中的程序,在执行a = 2时,程序仅仅是覆盖了a的值,但不会去更新临时栈中的那个要返回的值。执行完之后,就会通知主程序“finally的程序执行完毕,可以请求返回了”,这时,就会将临时栈中的值取出来返回。这下应该清楚了,要返回的值是保存至临时栈中的。
再来看一个例子,稍微改下上面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Test {
public static void main(String[] args) {
System.out.println(beforeFinally());
}

public static int beforeFinally(){
int a = 0;
try{
a = 1;
return a;
}finally{
a = 2;
return a;
}
}
}
/**output:
2
*/

在这里,finally{}里也有一个return,那么在执行这个return时,就会更新临时栈中的值。同样,在执行完finally之后,就会通知主程序请求返回了,即将临时栈中的值取出来返回。故返回值是2.

参考资料

  1. Java编程思想
  2. 深入理解Java中的final关键字