【Java面试题系列】:Java中final finally finalize的区别

518 阅读10分钟

按我的个人理解,这个题目本身就问的有点问题,因为这3个关键字之间没啥关系,是相对独立的,我猜想这道题的初衷应该是想了解面试者对Java中final finally finalize的使用方法的掌握情况,只是因为3个关键字比较像,而成了现在网上流传的题目“Java中final finally finalize的区别”。

既然是想了解面试者对Java中final finally finalize的使用方法的掌握情况,那么本篇我们就分别讲解下final,finally,finalize的使用方法。

1.final用法

我们先看下final的英文释义:最终的;决定性的;不可更改的,不禁要推测被final修饰的变量,方法或者类是不是不可修改的呢?

1.1final修饰类

在Java中,被final修饰的类,不能被继承,也就是final类的成员方法没有机会被继承,也没有机会被重写。

在设计类的时候,如果这个类不需要有子类,类的实现细节不允许改变,那么就可以设计为final类。

如我们在开发中经常使用的String类就是final类,以下为部分源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    ......
}    

1.2final修饰方法

在Java中,被final修饰的方法,可以被继承,但不能被子类重写(覆盖)。

在设计方法时,如果这个方法不希望被子类重写(覆盖),那么就可以设计为final方法。

举个具体的例子,我们新建个父类Animal如下:

package com.zwwhnly.springbootdemo;

public class Animal {
    public void eat() {
        System.out.println("Animal eat.");
    }

    public void call() {
        System.out.println("Animal call.");
    }

    public final void fly() {
        System.out.println("Animal fly.");
    }

    private final void swim() {
        System.out.println("Animal swim.");
    }
}

然后定义一个子类Cat继承Animal类,代码如下:

package com.zwwhnly.springbootdemo;

public class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("Cat eat.");
    }

    @Override
    public void fly() {
        System.out.println("Cat fly.");
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.eat();
        cat.call();
        cat.fly();
        cat.swim();
    }
}

我们会发现,以上代码中有2个错误

1)当我们重写fly()方法时,因为父类的fly()方法被定义为final方法,重写时会编译错误

2)cat.swim();报错,因为父类的swim()方法被定义为private,子类是继承不到的

然后我们将报错的代码删除,运行结果如下:

Cat eat.

Animal call.

Animal fly.

也就是eat()方法被子类重写了,继承了父类的成员方法call()和final方法fly()。

但是值得注意的是,在子类Cat中,我们是可以重新定义父类的私有final方法swim()的,不过此时明显不是重写(你可以加@Override试试,会编译报错),而是子类自己的成员方法swim()。

package com.zwwhnly.springbootdemo;

public class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("Cat eat.");
    }

    public void swim() {
        System.out.println("Cat swim.");
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.eat();
        cat.call();
        cat.fly();
        cat.swim();
    }
}

此时的运行结果为:

Cat eat.

Animal call.

Animal fly.

Cat swim.

1.3final修饰成员变量

用final修饰的成员变量没有默认值,可以在声明时赋值或者在构造函数中赋值,但必须赋值且只能被赋值1次,赋值后无法修改。

我们修改下1.2中的Cat类代码,定义2个final成员变量,1个声明完立即赋值,1个在构造函数中赋值:

package com.zwwhnly.springbootdemo;

public class Cat extends Animal {
    private final int age = 1;
    private final String name;

    public Cat(String name) {
        this.name = name;
    }

    @Override
    public void eat() {
        System.out.println("Cat eat.");
    }

    public static void main(String[] args) {
        Cat whiteCat = new Cat("小白");
        whiteCat.age = 2;
        System.out.println(whiteCat.age);
        System.out.println(whiteCat.name);

        Cat blackCat = new Cat("小黑");
        blackCat.name = "小黑猫";
        System.out.println(blackCat.age);
        System.out.println(blackCat.name);
    }
}

以上代码有2个编译错误,1个是whiteCat.age = 2;修改成员变量age时,另1个是blackCat.name = "小黑猫";修改成员变量name时,都提示不能修改final成员变量。

删除掉错误的代码,运行结果如下:

1

小白

1

小黑

1.4final修饰局部变量

被final修饰的局部变量,既可以在声明时立即赋值,也可以先声明,后赋值,但只能赋值一次,不可以重复赋值。

修改下Cat类的eat()方法如下:

@Override
public void eat() {

    final String breakfast;
    final String lunch = "午餐";
    breakfast = "早餐";
    lunch = "午餐2";
    breakfast = "早餐2";

    System.out.println("Cat eat.");
}

以上代码中2个错误,1个是lunch = "午餐2";,1个是breakfast = "早餐2";,都是对final局部变量第2次赋值时报错。

1.5final修饰方法参数

方法参数其实也是局部变量,因此final修饰方法参数和1.4中final修饰局部变量的使用类似,即方法中只能使用方法的参数值,但不能修改参数值。

在Cat类中新增方法printCatName,将方法参数修饰为final:

public static void main(String[] args) {
    Cat whiteCat = new Cat("小白");
    whiteCat.printCatName(whiteCat.name);
}

public void printCatName(final String catName) {
    //catName = "修改catName";    // 该行语句会报错
    System.out.println(catName);
}

运行结果:

小白

2.finally用法

提起finally,大家都知道,这是Java中处理异常的,通常和try,catch一起使用,主要作用是不管代码发不发生异常,都会保证finally中的语句块被执行。

你是这样认为的吗?说实话,哈哈。

那么问题来了,finally语句块一定会被执行吗?,答案是不一定

让我们通过具体的示例来证明该结论。

2.1在 try 语句块之前返回(return)或者抛出异常,finally不会被执行

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of test():" + test());
    }

    public static int test() {
        int i = 1;
        /*if (i == 1) {
            return 0;
        }*/
        System.out.println("the previous statement of try block");
        i = i / 0;
        try {
            System.out.println("try block");
            return i;
        } finally {
            System.out.println("finally block");
        }
    }
}

运行结果如下:

也就是说,以上示例中,finally语句块没有被执行。

然后我们将上例中注释的代码取消注释,此时运行结果为:

return value of test():0

finally语句块还是没有被执行,因此,我们可以得出结论:

只有与 finally 相对应的 try 语句块得到执行的情况下,finally 语句块才会执行。

以上两种情况,都是在 try 语句块之前返回(return)或者抛出异常,所以 try 对应的 finally 语句块没有执行。

2.2与 finally 相对应的 try 语句块得到执行,finally不一定会被执行

那么,与 finally 相对应的 try 语句块得到执行的情况下,finally 语句块一定会执行吗?答案仍然是不一定。

看下下面这个例子:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of test():" + test());
    }

    public static int test() {
        int i = 1;
        try {
            System.out.println("try block");
            System.exit(0);
            return i;
        } finally {
            System.out.println("finally block");
        }
    }
}

运行结果为:

try block

finally语句块还是没有被执行,为什么呢?因为我们在try语句块中执行了System.exit(0);,终止了Java虚拟机的运行。当然,一般情况下,我们的应用程序中是不会调用System.exit(0);的,那么,如果不调用这个方法,finally语句块一定会被执行吗?

答案当然还是不一定,当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。当然,死机或者断电属于极端情况,在这里只是为了证明,finally语句块不一定会被执行。

2.3try语句块或者catch语句块中有return语句

如果try语句块中有return语句, 是return语句先执行还是finally语句块先执行呢?

带着这个问题,我们看下如下这个例子:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        try {
            System.out.println("try block");
            return;
        } finally {
            System.out.println("finally block");
        }
    }
}

运行结果:

try block

finally block

结论:finally 语句块在 try 语句块中的 return 语句之前执行。

如果catch语句块中有return语句,是return语句先执行还是finally语句块先执行呢?

带着这个问题,我们看下如下这个例子:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of test():" + test());
    }

    public static int test() {
        int i = 1;
        try {
            System.out.println("try block");
            i = i / 0;
            return 1;
        } catch (Exception e) {
            System.out.println("catch block");
            return 2;
        } finally {
            System.out.println("finally block");
        }
    }
}

运行结果:

try block

catch block

finally block

return value of test():2

结论:finally 语句块在 catch 语句块中的 return 语句之前执行。

通过上面2个例子,我们可以看出,其实 finally 语句块是在 try 或者 catch 中的 return 语句之前执行的。更加一般的说法是,finally 语句块应该是在控制转移语句之前执行,控制转移语句除了 return 外,还有 break ,continue和throw。

2.4其它几个例子

示例1:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of getValue():" + getValue());
    }

    public static int getValue() {
        try {
            return 0;
        } finally {
            return 1;
        }
    }
}

运行结果:

return value of getValue():1

示例2:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of getValue():" + getValue());
    }

    public static int getValue() {
        int i = 1;
        try {
            return i;
        } finally {
            i++;
        }
    }
}

运行结果:

return value of getValue():1

也许你会好奇,应该会返回2,怎么返回1了呢?可以借鉴下以下内容来理解(牵扯到了Java虚拟机如何编译finally语句块):

实际上,Java 虚拟机会把 finally 语句块作为 subroutine(对于这个 subroutine 不知该如何翻译为好,干脆就不翻译了,免得产生歧义和误解。)直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值到本地变量表(Local Variable Table)中。待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。请注意,前文中我们曾经提到过 return、throw 和 break、continue 的区别,对于这条规则(保留返回值),只适用于 return 和 throw 语句,不适用于 break 和 continue 语句,因为它们根本就没有返回值。

示例3:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of getValue():" + getValue());
    }

    public static int getValue() {
        int i = 1;
        try {
            i = 4;
        } finally {
            i++;
            return i;
        }
    }
}

运行结果:

return value of getValue():5

示例4:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println("return value of getValue():" + getValue());
    }

    public static int getValue() {
        int i = 1;
        try {
            i = 4;
        } finally {
            i++;
        }
        return i;
    }
}

运行结果:

return value of getValue():5

示例5:

package com.zwwhnly.springbootdemo;

public class FinallyTest {
    public static void main(String[] args) {
        System.out.println(test());
    }

    public static String test() {
        try {
            System.out.println("try block");
            return test1();
        } finally {
            System.out.println("finally block");
        }
    }

    public static String test1() {
        System.out.println("return statement");
        return "after return";
    }
}

try block

return statement

finally block

after return

2.5总结

  1. finally语句块不一定会被执行
  2. finally 语句块在 try 语句块中的 return 语句之前执行。
  3. finally 语句块在 catch 语句块中的 return 语句之前执行。
  4. 注意控制转移语句 return ,break ,continue,throw对执行顺序的影响

3.finalize用法

finalize()是Object类的一个方法,因此所有的类都继承了这个方法。

protected void finalize() throws Throwable { }

finalize()主要用于在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。

子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。

当垃圾回收器(GC)决定回收某对象时,就会运行该对象的finalize()方法。

不过在Java中,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说filalize()可能永远不被执行,显然指望它做收尾工作是靠不住的。