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 相当于析构函数,不过一般不需要自己来实现这个方法,也尽量不要用这个方法。用户可以自己调用这个方法,但是只是正常的方法调用,与对象的销毁过程无关。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于