java面试整理(二)——final,finally,finalize

final,finally,finalize这三个看似一样,其实三个是完全不是一系列的东西。

重点内容整理

final

  1. 该关键字是一个修饰语,用于修饰成员变量、类、类或对象的引用、方法
  2. final修饰变量:final修饰变量说明该变量就是一个常量值,不能再修改,只能赋值一次
  3. final修饰类:使用final修饰类的功能是表明该类是一个最终类,其不能在被继承,也就是说我们不能写一个子类来继承一个final类。
  4. 当局部内部类访问某个局部变量,这个局部变量必须用final 关键字修饰。
  5. final修饰方法:修饰方法的final含义不是“最终的、不可修改的”,而是指该方法不可以被其子类重写。
  6. final修饰类或对象的引用:此方式只能保证其引用的地址不变,而不能保证其引用的内容不变。

finally

  1. 这个关键子用于在我门处理异常时,为了保证一些代码即便方法抛出异常也会执行
  2. finally不会执行的情况:(1)try中的语句没有执行(2)当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),比如 System.exit(0),或者断电、死机等
  3. try-catch-finally执行顺序:try代码块->catch代码块->finally代码块->finally返回语句->catch返回语句->try返回语句,注:try返回语句在执行之前会将返回值保存到本地变量表内,下面的语句对返回值的修改不会影响其返回值。

finalize

  1. 该关键字是一个方法finalize(),其是垃圾回收器(gc)要回收对象的时候,首先要调用这个类的finalize()
  2. 一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存.所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作.
  3. finalize()在什么时候被调用:(1)所有对象被Garbage Collection(GC)时自动调用,比如运行System.gc()的时候. (2)程序退出时为每个对象调用一次finalize方法。 (3)显式的调用finalize方法
  4. 当某个对象被系统收集为无用信息的时候,finalize()将被自动调用,但是jvm不保证finalize()一定被调用,

final

该关键字是一个修饰语,用于修饰成员变量、类、类或对象的引用、方法。

final修饰变量:final修饰变量说明该变量就是一个常量值,既可以是成员变量又可以是局部变量。其只能赋值一次,也就是说你不能在程序的任何地方在赋值后再修改该变量的值,如java.lang.Math类中的PI和E是final成员,其值为3.141592653589793和2.718281828459045。这两个值我们在使用时是不能够修改的,只能引用。
注:成员变量必须在声明时初始化值,是因为成员变量在这个类一被调用就初始化了,如果你没有对final修饰过的成员变量赋值,编译器初始化这个类的时候发现一个不可被修改的变量没有值,编译器必然报错。而局部变量可以在声明时不初始化,可以在下面赋值一次,但是第二次赋值会报错。

//1
public class A {
    final String a = "test";
    public final void f() {
       a = "test1";//此处会报错,不能修改final成员变量的值
    }
}
//2
public class test {
    public String a(){
        final String a;
        a = "s";
        a = "e";//此处会报错
        return a;
    }
}

final修饰类:使用final修饰类的功能是表明该类是一个最终类,其不能在被继承,也就是说我们不能写一个子类来继承一个final类。比如我们经常使用的String类就是使用final修饰的,所以我们不可能继承String类。

public final class A {}
// 编译错误!A是final类型,不可被继承!
public class B extends A{}

注:当局部内部类访问某个局部变量,这个局部变量必须用final 关键字修饰。
方法中定义的内部类,称之为:局部内部类。
这是因为局部变量的作用域是在 “方法的范围内”,这时候如果我们在方法内定义一个局部内部类,导致方法执行完成之后,但是局部内部类还在执行(在方法中定义一个线程),这样就会导致在方法完成之后,其局部变量并不会被回收,而是依然存在,这就导致了其作用域扩大。
因此,如果局部变量不用 final 修饰,我们就可以在(局部)内部类中随意修改该局部变量值,而且是在 该局部变量的作用域范围之外可以看到这些修改后的值。这会导致一些问题

final修饰方法:修饰方法的final含义不是“最终的、不可修改的”,而是指该方法不可以被其子类重写。

//父类test有两个方法一个是用final修饰的a(),一个是普通的b()
public class test {
    public final  String a(){
        final String a;
        a = "s";
        return a;
    }
    public String  b(){
        return null;
    }
}
//testA方法继承test类,其只能重写普通的方法b(),而不能重写final修饰的a().
public class testA extends test {

    public String a(){}//此处重写父类a()方法报错
    @Override
    public String b() {
        return super.b();
    }
}

此外,当一个方法被修饰为final方法时,意味着编译器可能将该方法用内联(inline)方式载入,所谓内联方式,是指编译器不用像平常调用函数那样的方式来调用方法,而是直接将方法内的代码通过一定的修改后copy到原代码中。这样可以让代码执行的更快(因为省略了调用函数的开销),比如在int[] arr = new int[3]调用arr.length()等。

另一方面,私有方法也被编译器隐式修饰为final,这意味着private final void f()和private void f()并无区别。

final修饰类或对象的引用 : 在java中我们是不能直接使用final修饰符来修饰一个对象的,我们只能修饰其引用,也就是比如public final A a = new A(); 这种方式其实没有什么意义,因为此方式只能保证其引用的地址不变,而不能保证其引用的内容不变,这个说起来可能比较抽象,我们可以使用一张图来说明:
这里写图片描述
如图所示,我们使用final修饰引用,只能保证其内部的引用对象的地址不变,而不能保证其引用对象中的内容不变,比如上面的public final A a = new A();,这时候我们修改引用的对象(new A())中的一个成员变量,是可以修改成功的,亦或者public final int[] a = {1, 2, 3, 4, 5},我们也是可以修改其数组里面的内容的。数组这个主要是因为虽然我们这样写好像是一个常量一样,其实其还是新创建了一个数组对象。

finally

这个关键子用于在我门处理异常时,为了保证一些代码即便方法抛出异常也会执行,其格式如下:

try{
}catch(Exption e){
}finally{
....
}

这时候即使catch抛出了异常,finally中的代码依然后执行。只是我们所共识的。

但是下面我们来说几个即使有finally语句也不会执行的情况:
1.try中的语句没有执行,则finally中的代码不会执行,比如下面的代码:

        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代码不会执行,
如果打开屏蔽的代码,在执行try语句前就返回,finally代码依然不会执行。

2.当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。
比如下面的

    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语句中我们使用了 System.exit(0)来终止java虚拟机的运行,则finally中的内容不会执行,其他情况下导致执行try catch的线程中断的情况,比如断电、死机等等。finally中的均不会执行。

那么它和 try 语句块与 catch 语句块的执行顺序又是怎样的呢?还有,如果 try 语句块或catch语句块中有 return 语句,那么 finally 语句块是在 return 之前执行,还是在 return 之后执行呢?这个也是经常会问到的问题。

首先我们先解决第一个问题,即try语句块,和catch语句块中不包含控制转移语句(return、break、continue)时的执行顺序,其执行顺序是:如果 try 语句块正常结束,那么在 try 语句块中的语句都执行完之后,再执行 finally 语句块。如果 try 语句块异常结束,应该先去相应的 catch 块做异常处理,然后执行 finally 语句块。

现在我们讲解下try语句块,和catch语句块中包含控制转移语句(return、break、continue)时的执行顺序:finally 语句块是在 try 或者 catch 中的 控制转移语句(return、break、continue)之前执行的。
大家可以运行下面两段代码来测试一下

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

    public static int test(){ 
        int i = 1;        
        try {  
            System.out.println("try block");  
            i = 1 / 0; 
            return 1;  
        }catch (Exception e){ 
            System.out.println("exception block"); 
            return 2; 
        }finally {  
            System.out.println("finally block");  
        } 
    } 
}
public class Test { 
    public static void main(String[] args) {  
        try {  
            System.out.println("try block");  
            return ;  
        } finally {  
            System.out.println("finally block");  
        }  
    }  
}

大家是不是觉得finally的用法以及执行顺序大家都掌握了,但是还会有另外的情况,大家可以看下面的代码

public class Test { 
    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

finally 语句块是在 try 或者 catch 中的 return 语句之前执行的。 由此,可以轻松的理解上面的的执行结果是 1。因为 finally 中的 return 1;语句要在 try 中的 return 0;语句之前执行,那么 finally 中的 return 1;语句执行后,把程序的控制权转交给了它的调用者 main()函数,并且返回值为 1。

public class Test { 
    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

按照上面的的分析逻辑,finally 中的 i++;语句应该在 try 中的 return i;之前执行啊? i 的初始值为 1,那么执行 i++;之后为 2,再执行 return i;那不就应该是 2 吗?怎么变成 1 了呢?

实际上,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 语句,因为它们根本就没有返回值。

我们来研究下上面例子的字节码:

public static int getValue(); 
 Code: 
  0:    iconst_1 
  1:    istore_0 
  2:    iload_0 
  3:    istore_1 
  4:    iinc    0, 1 
  7:    iload_1 
  8:    ireturn 
  9:    astore_2 
  10:   iinc    0, 1 
  13:   aload_2 
  14:   athrow 
 Exception table: 
  from   to  target type 
    2     4     9   any 
    9    10     9   any 
}

我们来分析一下其执行顺序:分为正常执行(没有 exception)和异常执行(有 exception)两种情况。我们先来看一下正常执行的情况,如图 1 所示:
这里写图片描述
我们跟着其代码来看:

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

首先我们定义一个int变量i = 1,这时候执行的操作是上图的iconst_1和instore_0,用于将定义的局部变量存入到本地变量表,这时候调用try中的代码需要 i 这个变量,从本地变量表中取出数据到操作数桟中(iload_0),这时候发现要执行的是一个返回语句,根据上面的虚拟机执行原则,其会先将返回值存入到本地变量中(istore_1),然后执行finally语句块的内容,因为其操作的是变量 i 的值,所以其修改的是本地变量表的序号0的值(iinc 0,1),在然后执行try中的return语句,将前面存储的返回值读取到操作数栈中,并返回(iload_1),也就是返回1。

下面我们来看看有异常时的执行情况:
从 getValue()方法的字节码中,我们可以看到它的异常处理表(exception table), 如下:

 Exception table: 
  from   to  target type 
    2     4     9   any 
    9    10     9   any 

它的意思是说:如果从 2 到 4 这段指令出现异常,则由从 9 开始的指令来处理。
这里写图片描述
先说明一点,上图中的 exception 其实应该是 exception 对象的引用,为了方便说明,我直接把它写成 exception 了。
当从 2 到 4 这段指令出现异常时,将会产生一个 exception 对象,并且把它压入当前操作数栈的栈顶。接下来是 astore_2 这条指令,它负责把 exception 对象保存到本地变量表中 2 的位置,然后执行 finally 语句块,待 finally 语句块执行完毕后,再由 aload_2 这条指令把预先存储的 exception 对象恢复到操作数栈中,最后由 athrow 指令将其返回给该方法的调用者(main)。

有了上面的例子,我们可以推测一下几个示例的结果:

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

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

这个中我们可以看到,其执行顺序,首先定义变量i,将i赋值1,也就是把1放入本地变量表0位置处,这时候执行try中语句,赋值4,修改0位置的变量的值,然后执行finally语句,因为其不是一个返回语句,则在执行finally语句前,并不会将值存入到本地变量表的1位置处,finally中执行i++,i值变为5,然后直接返回,所以其返回给调用者的值为5.

public class Test { 
    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语句移出到外面了,但是其执行顺序是一致的。这里就不做解释了

public class Test { 
    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

这个根据上面的代码大家可能有点不理解,但是return test1()这个代码你分解成String tmp = test1(); return tmp;这样我想大家应该就比较好理解了。

从这里大家可以看出虽然只是一个finally关键字,但是其内含的知识很多。。

finalize

该关键字是一个方法finalize(),其是垃圾回收器(gc)要回收对象的时候,首先要调用这个类的finalize(),一般的纯Java编写的Class不需要重新覆盖这个方法,因为Object已经实现了一个默认的,除非我们要实现特殊的功能。

不过用Java以外的代码编写的Class(比如JNI,C++的new方法分配的内存),垃圾回收器并不能对这些部分进行正确的回收,这时就需要我们覆盖默认的方法来实现对这部分内存的正确释放和回收(比如C++需要delete)。

总之,finalize相当于析构函数,他是垃圾回收器回收一个对象的时候第一个要调用的方法。不过由于Java的垃圾回收机制能自动为我们做这些事情,所以我们在一般情况下是不需要自己来手工释放的。

有时当撤消一个对象时,需要完成一些操作。例如,如果一个对象正在处理的是非Java 资源,如文件句柄或window 字符字体,这时你要确认在一个对象被撤消以前要保证这些资源被释放。为处理这样的状况,Java 提供了被称为收尾(finalization )的机制。使用该机制你可以定义一些特殊的操作,这些操作在一个对象将要被垃圾回收程序释放时执行。

要给一个类增加收尾(finalizer ),你只要定义finalize ( ) 方法即可。Java 回收该类的一个对象时,就会调用这个方法。在finalize ( )方法中,你要指定在一个对象被撤消前必须执行的操作。垃圾回收周期性地运行,检查对象不再被运行状态引用或间接地通过其他对象引用。就在对象被释放之 前,Java 运行系统调用该对象的finalize( ) 方法。

  finalize()方法的通用格式如下:

  protected void finalize( )
        {
        // finalization code here
        }

  理解finalize( ) 正好在垃圾回收以前被调用非常重要。例如当一个对象超出了它的作用域时,finalize( ) 并不被调用。这意味着你不可能知道finalize( )何时——甚至是否被调用。因此,你的程序应该提供其他的方法来释放由对象使用的系统资源,而不能依靠finalize( ) 来完成程序的正常操作。

  注意:如果你熟悉C++,那你知道C++允许你为一个类定义一个撤消函数(destructor ),它在对象正好出作用域之前被调用。Java不支持这个想法也不提供撤消函数。finalize() 方法只和撤消函数的功能接近。当你对Java 有丰富经验时,你将看到因为Java使用垃圾回收子系统,几乎没有必要使用撤消函数。

finalize的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存.所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作.

finalize()在什么时候被调用?
有三种情况
1.所有对象被Garbage Collection(GC)时自动调用,比如运行System.gc()的时候.
2.程序退出时为每个对象调用一次finalize方法。
3.显式的调用finalize方法

除此以外,正常情况下,当某个对象被系统收集为无用信息的时候,finalize()将被自动调用,但是jvm不保证finalize()一定被调用,也就是说,finalize()的调用是不确定的,这也就是为什么sun不提倡使用finalize()的原因

垃圾收集器在进行垃圾收集的时候会自动呼叫对象的finalize方法,用来进行一些用户自定义的非内存清理工作,因为垃圾收集器不会处理内存以外的东西。所以,有的时候用户需要定义一些清理的方法,比如说处理文件和端口之类的非内存资源。

总结:从上面分别分析三个关键字,我想大家应该都能够了解三个关键字的区别了吧

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值