天天看点

Java八股文:final、finally、finalize之间有什么区别

作者:尚硅谷教育

final、finally、finalize他们三者的区别,是一道再经典不过的面试题,我们在各个公司的面试题中几乎都能看到它的身影。final、finally和finalize虽然长得像孪生兄弟一样,但是它们的含义和用法却是大相径庭。final是Java中的一个关键字,修饰符;finally是Java的一种异常处理机制;finalize是Java中的一个方法名。接下来,我们具体说一下他们三者之间的区别。

一、final

1.1 修饰变量,包含静态和非静态

如果final修饰的是一个基本类型,就表示这个变量被赋予的值是不可变的,即它是个常量。

Java八股文:final、finally、finalize之间有什么区别

如图所示final修饰的a,之后再对a修改是无法修改的,会报编译错误。

如果final修饰的是一个对象,就表示这个变量被赋予的引用是不可变的。这里需要提醒大家注意的是,不可改变的只是这个变量所保存的引用,并不是这个引用所指向的对象。

下面我们测试一下,先定义一个Pet实体类

public class Pet {

private String name;

public Pet() {

}

public Pet(String name) {

this.name = name;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

}

Java八股文:final、finally、finalize之间有什么区别

由上图测试可见:不可改变的只是这个变量所保存的引用,该对象内容还是可以修改的。

当然关注final还有一些细节大家需要注意,如果一个变量或方法参数被final修饰,就表示它只能被赋值一次,但是JAVA虚拟机为变量设定的默认值不记作一次赋值。被final修饰的变量必须被初始化。

那接下来我们简单说一下初始化的几种情况:

  • 在定义的时候初始化

public class User1 {

private final String name = "Lucy";

}

  • 非静态final修饰变量可以在初始化块中初始化,不可以在静态初始化块中初始化;而静态final修饰的变量可以在静态初始化块中初始化,不可以在初始化块中初始化。

public class User2 {

private final String name;

private static final int a1;

{

name="Lucy";

}

static {

a1=1;

}

//经过代码测试a2在静态代码块无法进行初始化

/* private final int a2;

static {

a2=1;

}*/

//经过测试,静态的变量name2不能在初始化代码块中初始化

/* private static final String name2;

{

name2="Lucy";

}*/

}

  • final变量还可以在类的构造器中初始化,但是静态final变量不可以。

public class User4 {

private final String name;

//经过测试验证静态修饰的a1无法在构造器中进行初始化

private static final int a1;

public User4() {

name="11";

a1=1;

}

}

以上就是初始化的3种情况,大家简单的认知一下。

那有人会问了:JVM对于声明为final的局部变量做了那些性能优化呢?在能够通过编译的前提下,无论局部变量声明时带不带final关键字修饰,对其访问的效率都一样,我们来进行测试一下:

如下所示不带final的情况:

static int demo1() {

int a = ValueA();

int b = ValueB();

return a + b;

}

带final的情况:

static int demo2() {

final int a = ValueA();

final int b = ValueB();

return a + b;

}

他们两者之间通过Javac编译后的得到的结果是一样的:

Code:

0:invokestatic #2 //Method ValueA:()

3:istore_0 // 设置a的值

4:invokestatic #3 //Method ValueB:()

7:istore_1 // 设置b的值

8:iload_0 // 读取a的值

9:iload_1 // 读取b的值

10:iadd

11:ireturn

根据字节码可见文件里除字节码外的辅助数据结构也没有记录任何体现final的信息。既然带不带final的局部变量在编译到Class文件后都一样了,其访问效率必然一样高,JVM不可能有办法知道什么局部变量原本是用final修饰来声明的。

当然还是有特殊情况的出现:那就是我们声明的局部变量并不是一个变量,如下所示的两种情况:

public class Test01 {

static int foo(){

final int a = 11;

final int b = 12;

return a + b;

}

static int foo1(){

int a = 11;

int b = 12;

return a + b;

}

}

他们两者之间通过Javac编译后得到的字节码如下所示:

Java八股文:final、finally、finalize之间有什么区别

foo()方法中,这样的话实际上a和b都不是变量,而是编译时常量,其访问会按照Java语言对常量表达式的规定而做常量折叠。foo1()方法中a和b都是去掉final修饰,那么a和b就会被看作普通的局部变量而不是常量表达式,因此在字节码层面上的效果会不一样。

其实这种层面上的差异只对比较简易的 JVM 影响较大,因为这样的 JVM 对解释器的依赖较大,原本 Class 文件里的字节码是怎样的它就怎么执行;对高性能的 JVM则没啥影响。所以,大部分 final 对性能优化的影响,可以直接忽略,我们使用 final 更多的考量在于其不可变性。

1.2 修饰方法

当定义一个方法被final修饰时,表示此方法不可以被子类重写,但是依旧可以被子类继承使用,接下来我们做验证:

先在父类定义一个final修饰的方法:

public class Fu {

public final void add(){

System.out.println("这是父类final修饰的一个方法");

}

}

子类重写父类final修饰的方法,并且子类对象调用此方法:

public class ZI extends Fu{

/* public void add(){

System.out.println("子类重写父类final修饰的方法");

}*/

public static void main(String[] args) {

ZI zi = new ZI();

zi.add();

}

}

经过如上代码可知,子类重写父类final修饰的方法会报编译异常,无法进行重写此方法,但是子类依旧可以调用此方法。

1.3 修饰类

其实用于final修饰的类我们很熟悉,因为我们最常用的String类就是final修饰的,由于final类不允许被继承,就意味着它不能再派生出新的子类,不能作为父类被继承,因此,一个类不能同时被声明为abstract抽象类和final类。编译器在处理时把它的所方法都当作final的,因此final类比普通类拥更高的效率。

final的类的所方法都不能被重写,但这并不表示final的类的属性(变量值)也是不可改变的,要想做到final类的属性值不可改变,必须给它增加final修饰。我们进行验证:

public final class User {

int a1=1;

final int a2 = 2;

public static void main(String[] args) {

User user = new User();

user.a1=3;

// user.a2=3;

System.out.println(user.a1);

}

}

经过如上代码测试所得,第7行a2被final修饰,不可以二次赋值,它是不可变的,而最后的输出结果也是3,证明a1的值发生了变化。所以final修饰的类并不表示其属性(变量值)也是不可改变的。

二、finally

finally是什么呢?是Java的一种异常处理机制。finally只能用在try/catch语句中,并且是附带着的一个语句块,表示这段语句最终总是被执行。请看下面代码:

public class FinallyTest {

public static void main(String[] args) {

try {

throw new NoSuchFieldException();

} catch (NoSuchFieldException e) {

System.out.println("该程序抛出异常");

} finally {

System.out.println("这里执行了finally中的代码");

}

}

}

运行结果如下:

该程序抛出异常

这里执行了finally中的代码

由结果可见finally内的代码被执行了。

那有同学会问,finally的代码一定会执行吗?return、continue、break这个可以打乱finally的顺序吗,那接下来我们做一下验证:

  • return是否能影响finally语句块的执行,看代码:

public class FinallyTest {

public static void main(String[] args) {

int i = returnTest();

System.out.println("i = " + i);

}

public static int returnTest() {

try {

return 1/0;

} catch (Exception e) {

System.out.println("该程序抛出异常");

} finally {

System.out.println("这里执行了finally中的代码");

}

return 1;

}

}

运行结果如下:

该程序抛出异常

这里执行了finally中的代码

i = 1

由结果可见,finally内的代码被执行了,因此return不会影响finally内的代码。

  • continue是否能影响finally语句块的执行,看代码:

public class FinallyTest {

public static void main(String[] args) {

continueTest();

}

public static void continueTest() {

for (int i = 0; i < 3; i++) {

try {

if (i==1){

continue;

}

System.out.println("i = " + i);

} catch (Exception e) {

System.out.println("该程序抛出异常");

} finally {

System.out.println("这里执行了finally中的代码");

}

}

}

}

运行结果如下:

i = 0

这里执行了finally中的代码

这里执行了finally中的代码

i = 2

这里执行了finally中的代码

由结果所示,当i==1时,条件成立,执行continue,所以i=1的输出打印被跳出,但是finally内的代码依旧被执行了,因此continue不会影响finally内的代码。

  • break是否能影响finally语句块的执行,看代码:

public class FinallyTest {

public static void main(String[] args) {

continueTest();

}

public static void continueTest() {

for (int i = 0; i < 3; i++) {

try {

if (i==1){

break;

}

System.out.println("i = " + i);

} catch (Exception e) {

System.out.println("该程序抛出异常");

} finally {

System.out.println("这里执行了finally中的代码");

}

}

}

}

运行结果如下:

i = 0

这里执行了finally中的代码

这里执行了finally中的代码

由结果所示,当i==1时,条件成立,执行break,所以后续代码被终止了,但是i==1时的finally内的代码依旧被执行了,因此break不会影响finally内的代码。

显而易见return、continue、break不会对finally内的代码造成影响,finally内的代码一定会被执行。

finally的本质是什么呢?那我们来看一下,我们就以如下代码为例:

public class Test01 {

public static void main(String[] args) {

int a1 = 0;

try {

a1 = 1;

}catch (Exception e){

a1 = 2;

}finally {

a1 = 3;

}

System.out.println(a1);

}

}

我们来看一下此段代码的字节码如图所示:

Java八股文:final、finally、finalize之间有什么区别

我们先解释一下 Exception table,他是一个异常表,其中的每一条都表示一个异常发生器,异常发生器由 From 指针,To 指针,Target 指针和应该捕获的异常类型构成。

根据如上图所示的字节码我们会发现finally会把“a1=3”的字节码 iconst_3 和 istore_1 放在了try块和catch块的后面。相当于在try代码块和catch的代码块的后面都含有一条a1 = 3,所以我们最终的结果一定会执行finally中的代码。

上面我们讨论的都是 finally 一定会执行的情况,当然值得一提的是finally也不是一定会被执行的。那就是当我们期间调用System.exit()方法,他就会不执行finally的内容,具体如下:

public static void main(String[] args) {

try {

System.out.println(" 执行try中的代码 " );

System.exit(0);

} catch (Exception e) {

System.out.println("该程序抛出异常");

} finally {

System.out.println("这里执行了finally中的代码");

}

}

运行结果如下:

执行try中的代码

Process finished with exit code 0

由结果显示,finally内的代码未执行,System.exit()这个方法是用来结束当前正在运行中的Java虚拟机。对于此方法大家有个印象就可以了,知道该方法会影响到finally的执行。

三、finalize

最后,我们来看一下finalize,他是一个方法,隶属于java.lang.Object类,方法定义如下:

protected void finalize() throws Throwable { }

此方法是GC运行机制的一部分,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。

在 Java 中,由于 GC 的自动回收机制,因而并不能保证 finalize 方法会被及时地执行(垃圾对象的回收时机具有不确定性),也不能保证它们会被执行。也就是说,finalize 的执行时期不确定,我们并不能依赖于 finalize 方法帮我们进行垃圾回收,可能出现的情况是在我们耗尽资源之前,gc 却仍未触发,所以推荐使用资源用完即显示释放的方式,比如 close 方法。

finalize 的工作方式是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将会首先调用 finalize 方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

总结

final、finally 和 finalize三者之间看着像孪生兄弟,但是他们三个之间没有任何的关系。final 是用来修饰类、变量、方法和参数的关键字,finally是异常处理语句结构的一部分,表示总是执行。finalize 是 Object 类中的一个基础方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收的。因此他们之间的区别还是显而易见的,希望此篇文章帮助读者更加深刻的区分。