Java 中 final、finally 和 finalize 使用总结

Java 中 final、finally 和 finalize 使用总结


1. final

1.1 final 修饰变量

final 用于修饰变量时表示该变量一旦被初始化就不可以再改变,这里的不可以再改变指的是,对于基本类型它们的值不能改变,对于对象变量它们的引用不可以改变,对于后者需要注意的是,只是引用不能改变,即指向初始化时的那个对象,对象中的属性等是可以改变的,例如我们有个 final 修饰的 ArrayList,那么这个变量只能是指向最开始初始化时的 ArrayList 对象,不能使它指向其他 ArrayList 对象,但是 ArrayList 中的值是可以改变的。

final 变量的初始化可以在定义处初始化,也可以在构造方法中初始化,另外还可以在代码块中初始化。

final 一般和 static 一起修饰变量用于表示常量,需要注意的是 static final 修饰变量和 final 修饰变量是有区别的,前者表示唯一不可改变,即类的所有对象共享一个变量,同时它在初始化后就不可以再改变,而后者指的是每个对象中各自有自己的变量,它们被初始化后就不能再改变。

final 修饰的成员变量不会有默认的初始化,而普通成员变量和静态成员变量会有默认的初始化,例如对于 int 默认初始化是 0,而引用类型默认初始化是 null 等。

局部内部类和匿名内部类中只能访问局部 final 变量,例如:

public class Test {
    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

我们可以尝试把 a 变量或者 b 变量的 final 给去掉,可以发现在高版本的 java 中是可以编译通过的,但是我们加一条修改变量的语句,就会出现编译错误,提示该变量需要是 final 修饰的。也就是高版本的 java 中编译器会默认帮我们添加 final 则个关键字。我们可以以上面的代码为例分析原因,当 test 方法执行完后 a 和 b 的生命周期就结束了,但是 Thread 可能还没有结束,也就意味着再次访问 a 或者 b 变量就不可能了,所以 java 中就用复制的方式来解决则个问题,即内部类中使用到的变量和方法中的变量不是同一个变量,它只是一份拷贝,虽然这样可以解决生命周期的问题,但是因为访问的变量和原本并不是同一个变量,如果修改就会出现不一致的问题,所以就需要把它设定为是 final 类型,让它不能够修改。

final 修饰能够保证对象的安全发布,即对象在初始化完成之前能够保证不被其他线程使用,它的原理是通过禁止 CPU 的指令集重排序。但是这样并不是能够保证线程安全,例如下面的代码:

public class Test{
      public static final List<Integer> list = new ArrayList<Integer>();
      public void add(Integer value){
          list.add(value);
      }
}

如果在多线程环境下,虽然能够保证 list 在没有被初始化之前不被其他线程访问到,但是里面的 add 方法可能会出现线程同步问题,需要给方法或者里面的代码段加锁。

1.2 final 修饰方法

当 final 修饰方法时表示该方法不能够被覆盖,但是该方法是可以被继承的;在 java 的早期版本中通过给方法添加 final 可以带来性能的提升,但是现在的 java 版本中是不需要通过这种方式来优化的;另外,类中的 private 方法会被隐式指定为 final 方法。

1.3 final 修饰类

当一个类被指定为 final 时表示这个类不能够被继承,类中的成员变量可以指定为 final,也可以不指定为 final,但是类中的方法都被隐式指定为 final 方法。

2. finally

finally 用于创建在 try-catch 块后面执行的代码块,即无论是否发生异常都会执行到 finally 块中的代码,但是这个不是绝对的,例如在 try 或者 catch 块中执行结束 JVM 进程的语句,如:System.exit(0),这时 finally 块中的代码是不会执行的,还有例如线程中断等情况。

在 finally 块中最好不要写 return 语句以免使人误解,例如下面的代码:

try{
  return 0;
}catch(Exception e){
  return 1;
}finally{
  return 2;
}

在上面的代码中,如果没有异常我们的返回值是什么,这个可能会使人误解,在这种情况下返回值应该是 2,因为 finally 块中的代码是在 try 或者 catch 块中的 return 语句执行之后真正返回之前执行的,所以 finally 块中的 return 语句就先返回了,另外再看一个例子:

i = 0;
try{
  i = 1;
  return i;
}catch(Exception e){
  i = 2;
  return i;
}finally{
  i = 3;
}

我们先考虑没有异常情况下的返回值是什么,通过上一个例子的分析,可能会认为这个例子的返回值是 3,但是事实上返回值是 1,这个和 finally 块的执行机制有关,可以把它看作类似方法的调用,我们知道当我们调用一个方法时,传入一个基本变量的值给方法的形参,我们在方法中改变这个形参是不影响原本的实参的,因为这是一个值传递的过程,传入形参的值只是原本实参的一个拷贝。同理,在 finally 块中也有类似的机制,所以我们在 finally 块中修改 i 的值是不会影响 try 块中的返回值的。

我们继续看一个例子:

public static Map<String,Integer> getMap(){
    Map<String,Integer> map = new HashMap<>();
    try{
      map.put("test",1);
      return map;
    }catch(Exception e){
      map.put("test",1);
      return map;
    }finally{
      map.put("test",3);
    }
 }
public static void main(String[] args) {
    System.out.println(getMap().get("test"));
}

通过上一个例子的分析可以很容易知道这个例子的输出是 3,这个例子和上一个例子不同的地方是一个是基本变量,一个是对象,这里也可以通过类比方法调用来理解,Java 中只有值传递没有地址传递,我们把一个对象传入方法的形参,我们传递的只是原本实参(保存有对象在内存中的地址)的一份拷贝,也就是相当于传递后有两个引用指向实际内存,那么我们通过形参修改当然可以修改到对象内容,但是如果我们使形参指向另一个对象,因为原本实参中对象的地址没有改变,所以原本对象并没有改变,而是形参自己指向了另一个对象而已。

3. finalize

finalize 是一个方法,该方法是在垃圾回收器回收对象时调用到的一个方法,可以通过覆盖的方式在这个方法中添加需要执行的代码,一般添加一些释放资源的代码。finalize 相当于析构函数,不过一般不需要自己来实现这个方法,也尽量不要用这个方法。用户可以自己调用这个方法,但是只是正常的方法调用,与对象的销毁过程无关。