Java 中 final、finally 和 finalize 使用总结

本贴最后更新于 2735 天前,其中的信息可能已经时过境迁

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 相当于析构函数,不过一般不需要自己来实现这个方法,也尽量不要用这个方法。用户可以自己调用这个方法,但是只是正常的方法调用,与对象的销毁过程无关。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • macbolo

    哈哈 java8 中匿名内部类不需要用 final 了