当前位置: 首页 > 科技观察

看完这篇 final、finally 和 finalize 和面试官扯皮就没问题了

时间:2023-03-19 19:15:37 科技观察

看完这篇文章finally,finally,finalize,再跟面试官争论意义就没问题了;finally也是一个关键字,但是我们可以使用finally和其他关键字进行一些组合操作;finalize是一个不受欢迎的方法,它是对象祖先Object中的方法,不再推荐使用finalize机制。在这篇文章中,cxuan将带你从这三个关键词入手,从用法、应用、原理的角度,带你深入浅出地了解这三个关键词。1.final、finally和finalize相信在座的各位都是经验丰富的程序员,关于final的基本关键字就不用多说了。不过,我们还是要照顾好小白读者,毕竟我们都是从小白出来的。(1)final修饰的类,属性和方法final可以用来修饰类,final修饰的类不允许其他类继承,也就是说final修饰的类是唯一的。如下图,我们先定义一个FinalUsage类,它使用final修饰,同时定义一个FinalUsageExtend类,它要继承(扩展)FinalUsage。我们如上继承后,编译器就不让我们这么玩了。提示Wecan'tinheritfromtheFinalUsageclass,why?别担心,这是一个Java约定,有一些不需要它的原因,只需遵循它。final可以用来修饰方法,final修饰的方法不允许被重写。我们来演示一下没有修饰final关键字的情况。如上图所示,我们使用FinalUsageExtend类继承FinalUsage类,提供writeArticle方法的重写。这个编译没有问题。重写的重点是@Override注解和方法修饰符、名称、返回值的一致性。注意:很多程序员在重写方法时会忽略@Override,这无疑会增加代码阅读的难度,不推荐。当我们使用final修饰方法时,这个方法不能被重写。如下所示,当我们将writeArticle方法声明为void时,被重写的方法会报错,writeArticle方法不能被重写。final可以修饰变量,final修饰的变量一旦定义就不能再修改了。如下图,编译器提示的错误是无法继承final修饰的类。上面我们使用的是字符串String,String默认是final的。其实不使用final修饰意义不大,因为字符串不能改写,这并不能说明问题。让我们重写它并使用基本数据类型来演示同样的事情。编译器还是给出了age不能改写的提示,证明final修饰的变量是不能改写的。Java中不仅有基本数据类型,还有引用数据类型,那么引用类型被final修饰后会怎样呢?让我们看下面的代码首先构造一个Person类:publicclassPerson{intid;Stringname;get()andset()...toString()...}然后我们定义一个最终的Person变量。staticfinalPersonperson=newPerson(25,"cxuan");publicstaticvoidmain(String[]args){System.out.println(person);person.setId(26);person.setName("cxuan001");System.out.println(person);}输出,你会发现一个奇怪的现象,为什么我们明明亲自改了id和name,编译器却没有报错?这是因为final修饰的引用类型只保证对象的引用不会改变。对象内的数据可以更改。这就涉及到对象在内存中的分配,后面会讲到。(2)finally保证程序会被执行。最后是一种确保程序将被执行的机制。同样,它也是Java中的关键字。一般来说,finally是不会单独使用的。一般和try块一起使用,比如下面是一个try...finally代码块:try{lock.lock();}finally{lock.unlock();}这是加锁的代码示例/解锁。锁上锁后,在finally操作中进行解锁,因为finally可以保证代码一定要执行,所以一般会把一些比较重要的代码放在finally中,比如解锁操作,流关闭操作,连接释放操作等。lock.lock()产生异常,它也可以与try...catch...finally一起使用:try{lock.lock();}catch(Exceptione){e.printStackTrace();}finally{lock.unlock();}try...finally这种写法适用于JDK1.7之前。在JDK1.7中,引入了一个新的关闭流的操作,即try...with...resources,这是Java引入的try-with-resources语句将try-catch-finally简化为try-catch,实际上是一种语法糖,而不是额外的语法。try...with...resources仍将在编译时转换为try-catch-finally语句。句法糖,又译为糖衣语法,是指在计算机语言中加入的某种语法。这种语法对语言的功能没有影响,但更方便程序员使用。一般来说,语法糖的使用可以增加程序的可读性,从而减少程序代码出错的几率。在Java中,有一些语法糖可以简化程序员的使用,后面再说。(3)finalize的作用finalize是其祖先类Object类的一个方法,其设计目的是保证对象在垃圾回收前完成特定资源的回收。不再推荐使用finalize,并且在JDK1.9中已明确标记为已弃用。2、深入理解final、finally和finalize(1)final设计很多编程语言都会有一些方式告诉编译器某条数据是常量。有时不可变数据很有用,例如永不更改的编译时常量。比如staticfinalintnum=1024是一个运行时初始化的值,你不想改变它的最终设计来和抽象设计冲突,因为abstract关键字主要修饰抽象类,需要定义抽象类通过特定的类。完成。final表示禁止继承,执行起来不会有问题。因为只有继承之后,子类才能实现父类的方法。类中的所有private都被隐式指定为final,在private修饰的代码中使用final没有额外的意义。(2)BlankfinalJava允许blankfinal。Blankfinal将声明称为final,但未为其赋值以对其进行初始化。但是无论如何,编译器都需要对final进行初始化,所以这个初始化的任务就交给了构造函数。Blankfinal为final提供了更大的灵活性。以下代码:publicclassFinalTest{finalIntegerfinalNum;publicFinalTest(){finalNum=11;}publicFinalTest(intnum){finalNum=num;}publicstaticvoidmain(String[]args){newFinalTest();newFinalTest(25);}}在不同的构造函数中初始化不同的final以利用finalNum更灵活。final的使用主要有两种方式:Immutable和EfficiencyImmutable:Immutable是指给方法加锁(注意不是加锁),重点是防止其他方法被改写。效率:这个主要针对Java的早期版本。在Java的早期实现中,如果将方法声明为final,意味着编译器会将对该方法的调用改为内联调用,但并不会带来显着的性能优化。这种叫法比较鸡肋。在Java5/6中,热点虚拟机自动检测嵌入式调用并进行优化。因此,使用final修改的主要方法有一种:immutable。注意:final不是Immutable,Immutable才是真正的不可变。final并不是真正的Immutable,因为final关键字引用的对象是可以改变的。如果我们真的想让对象不可变,通常需要相应的类来支持不可变行为,比如下面的代码:finalListfList=newArrayList();fList.add("Hello");fList.add("世界");ListunmodfiableList=List.of("你好","世界");unmodfiableList.add("再次");List.of方法创建一个不可变列表。Immutable不可变在很多情况下是一个不错的选择。一般来说,Immutable的实现需要注意以下几点:将类声明为final,防止其他类继承。在类内部声明成员变量(包括实例变量和类变量)为private或final,不提供可以修改成员变量的方法,即setter方法。在构造对象时,通常会使用深度克隆,这有助于防止其他人在直接为对象赋值时修改输入的对象。坚持写时复制原则,创建私有副本。(3)final能不能提升性能?final能否提升性能一直是业界争论的焦点。很多书上都有介绍在特定场景下可以提升性能。例如,final可用于帮助JVM内联方法,这些方法可以进行转换和编译。编译器的编译能力等等,但其中很多结论都是建立在假设之上的。粗略地说,无论局部变量是否使用final关键字声明,访问它的效率都是一样的。例如,以下代码(没有final的版本):staticintfoo(){inta=someValueA();intb=someValueB();returna+b;//在此处访问局部变量}带有final的版本:staticintfoo(){finalinta=someValueA();finalintb=someValueB();returna+b;//这里访问局部变量}用javac编译后得到的结果是完全一样的。invokestaticsomeValueA:()Iistore_0//设置a的值invokestaticsomeValueB:()Iistore_1//设置b的值iload_0//读取a的值iload_1//读取b的值iaddireturn因为上面是使用引用类型,所以字节码相同。如果是常量类型,我们看一下://withfinalstaticintfoo(){finalinta=11;finalintb=12;returna+b;}//withoutfinalstaticintfoo(){inta=11;intb=12;returna+b;}我们分别编译两个foo方法,会发现如下字节码:左边是非final关键字修饰的代码,右边是final关键字修饰的代码。对比这两个字节码,我们可以得出以下结论。无论是否有最后的修饰,inta=11或inta=12都被当作常量处理。在returnreturn时,没有final的a+b会被当作一个变量;a+b最终修改将直接视为常量。事实上,这种程度的差异只影响相对简单的JVM,因为这样的VM严重依赖解释器,它执行原始Class文件中的任何字节码;对于高性能的JVM(如HotSpot、J9等)影响不大。所以final对性能优化的大部分影响可以直接忽略。我们使用finalmore是因为它的不变性。(4)深入理解finally上面我们已经大致讲了finally的使用,它的作用是保证try块中的代码执行完后,finally中的语句才会执行。不管try块中是否抛出异常。那么我们就深入了解一下finally,finally的字节码是什么,finally什么时候执行的本质。首先我们知道finally块只有在try块执行时才会执行,finally不会单独存在。这个不用过多解释,这是大家都知道的规律。finally必须与try块或trycatch块一起使用。其次,finally块在try块执行完成或try块未完成后,在控制转移语句(return/continue/break)之前执行,但后面紧跟控制转移语句。以return为例,看看是不是这样。以下代码:staticintmayThrowException(){try{return1;}finally{System.out.println("finally");}}publicstaticvoidmain(String[]args){System.out.println(FinallyTest.mayThrowException());}从执行结果可以证明,finally应该在return之前执行。当finally有返回值时,直接返回。将不再返回try或catch中的返回值。staticintmayThrowException(){try{return1;}finally{return2;}}publicstaticvoidmain(String[]args){System.out.println(FinallyTest.mayThrowException());}在执行finally语句之前,控制转移语句会返回值看局部变量中的如下代码:));}上面的代码可以看出returni是在++i之前执行的,returni会暂存i的值和finally一起返回。(5)finally的本质先看一段代码:publicstaticvoidmain(String[]args){inta1=0;try{a1=1;}catch(Exceptione){a1=2;}finally{a1=3;}System.out.println(a1);}这段代码输出的结果是什么?答案是3,为什么?带着疑惑,我们来看一下这段代码字节码bytecode的中文注释。它已为您标记。这里需要注意下面的Exception表。异常表是一个异常表。异常表中的每个条目代表一个异常生成器。异常生成器由From指针、To指针、Target指针和应该捕获的Exception类型组成。因此,上述代码存在三种执行路径。如果try语句块中有属于exception及其子类的异常,则跳转到catch处理。如果try语句块中出现了不属于exception及其子类的异常,则跳转到finally处理如果catch语句块中出现了新的异常,则跳转到finally处理说到这里,我们还没说什么最后的本质是。仔细看上面的字节码,会发现finallya1=3的字节码iconst_3和istore_1会放在try块和catch块后面,所以上面的代码相当于:publicstaticvoidmain(String[]args){inta1=0;尝试{a1=1;//finallya1=3}catch(Exceptione){a1=2;//finallya1=3}finally{a1=3;}System.out.println(a1);}上面的Exception表只是Throwable异常的一个子类,error会执行异常走查的异常表。一般情况下,没有try块就没有异常表。让我们验证一下:publicstaticvoidmain(String[]args){inta1=1;System.out.println(a1);}比如上面我们用了一个很简单的程序来验证。编译之后,我们可以查看它的字节码,没有异常表。(6)最后会被执行吗?上面我们讨论了finally会被执行,那么finally会被执行吗?可能不会。除了机房停电、机房爆炸、机房进水、机房雷击、强行关机、拔掉电源外,还有几种情况可以阻止finally执行。调用System.exit方法调用Runtime.getRuntime().halt(exitStatus)方法JVM崩溃(滑稽脸)如果JVM在try或catch块中进入无限循环(或其他不间断的非终止语句)是否操作系统JVM进程被强行终止;例如,在UNIX上执行kill-9pid如果主机系统死机;如断电、硬件错误、OS崩溃等都不会执行如果finally块是由守护线程执行的,那么finally之前的Exit中的所有非守护线程都会被调用。(7)finalize真的没用吗?我们在上面简单介绍了finalize方法,并说明了它是一种不好的做法。那么什么时候调用finalize呢?为什么finalize没用?我们知道Java和C++的一个显着区别就是Java可以自动管理内存。在Java中,由于GC的自动回收机制,并不能保证finalize方法会被及时执行(垃圾对象回收的时机不确定),也不能保证它们一定会被执行。也就是说finalize的执行周期是不确定的,我们不能依赖finalize方法来帮助我们进行垃圾回收。可能会出现我们资源耗尽之前gc还没有触发的情况,所以推荐使用资源耗尽显示释放的方式,比如close方法。另外finalize方法也会把异常活吞。finalize的工作方式是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,就会先调用finalize方法,直到下一次垃圾回收动作发生时才会真正回收对象占用的内存.垃圾收集只是关于内存。我们不提倡在日常开发中使用finalize方法。可以使用finalize方法的地方,用try...finally会更好。