详解Java 中的 AutoCloseable 接口
一、前言
最近用到了JDK7中的新特性try-with-resources语法,感觉到代码相对简洁了很多,于是花了点时间详细学习了下,下面分享给大家我的学习成果。
二、简单了解并使用
try-with-resources语法比较容易使用,一般随便搜索看下示例代码就能用起来了。JDK对这个语法的支持是为了更好的管理资源,准确说是资源的释放。
当一个资源类实现了该接口close方法,在使用try-with-resources语法创建的资源抛出异常后,JVM会自动调用close方法进行资源释放;当没有抛出异常正常退出try代码块时也会自动调用close方法。像数据库链接类Connection,io类InputStream或OutputStream都直接或者间接实现了该接口。
下面我们通过代码示例来了解如何使用,首先创建一个实现了AutoCloseable接口的类:
publicclassResourceimplementsAutoCloseable{ publicvoidread(){ System.out.println("dosomething"); } @Override publicvoidclose()throwsException{ System.out.println("closed"); } }
1、不使用这个语法,须主动close关闭
publicstaticvoidf1(){ Resourceresource=newResource(); try{ resource.read(); }finally{ try{ resource.close(); }catch(Exceptione){ } } }
2、使用这个语法,代码更加优雅简洁
publicstaticvoidf2(){ try(Resourceresource=newResource()){ resource.read(); }catch(Exceptione){ } }
注意:read方法本身不抛异常,但是编码时idea提示必须要有catch块,因此可知这个异常的捕获是捕获的close方法抛出的异常。
3、改成Closeable接口,也可以
接着我们将Resource类上的AutoCloseable接口改为Closeable(如下),此时需要将close方法的异常签名改成IOException,否则编译不通过。注意下面代码主动抛出IOException目的为满足异常签名,否则idea提示要删掉异常签名,变成无异常签名。因此在实现Closeable接口后,异常签名要么没有,要么是IOException或者其子类。
publicclassResourceimplementsCloseable{ publicvoidread(){ System.out.println("dosomething"); } @Override publicvoidclose()throwsIOException{ System.out.println("closed"); thrownewIOException(); } }
接着我们看下Closeable和AutoCloseable关系,发现除了继承关系,Closeable只是将close方法的异常签名变得稍微具体了,从Exception变为IOException。
publicinterfaceAutoCloseable{ //省略Javadoc voidclose()throwsException; }
publicinterfaceCloseableextendsAutoCloseable{ ////省略Javadoc publicvoidclose()throwsIOException; }
因此无论是实现了JDK中的java.lang.AutoCloseable还是java.io.Closeable接口,都能使用try-with-resources语法。此处注意还有点不同的是两个接口的包路径的差异。
三、Javadoc阅读
1、AutoCloseable中的Javadoc
Anobjectthatmayholdresources(suchasfileorsockethandles)untilitisclosed.Theclose()methodofanAutoCloseableobjectiscalledautomaticallywhenexitingatry-with-resourcesblockforwhichtheobjecthasbeendeclaredintheresourcespecificationheader.Thisconstructionensurespromptrelease,avoidingresourceexhaustionexceptionsanderrorsthatmayotherwiseoccur.
Itispossible,andinfactcommon,forabaseclasstoimplementAutoCloseableeventhoughnotallofitssubclassesorinstanceswillholdreleasableresources.Forcodethatmustoperateincompletegenerality,orwhenitisknownthattheAutoCloseableinstancerequiresresourcerelease,itisrecommendedtousetry-with-resourcesconstructions.However,whenusingfacilitiessuchasjava.util.stream.StreamthatsupportbothI/O-basedandnon-I/O-basedforms,try-with-resourcesblocksareingeneralunnecessarywhenusingnon-I/O-basedforms.
以上是AutoCloseable的完整Javadoc,大概意思是:
像文件和socket等对象,在调用其close方法后才会释放持有的资源。当使用try-with-resources语法实例化一个实现了AutoCloseable接口的类的对象时,close()方法将会自动被调用,确保及时释放资源,避免可能发生的资源耗尽问题。
我们经常能见到一些基类实现了AutoCloseable接口,这是可行的,哪怕并不是所有的子类需要释放资源,或者哪怕并不是全部实例都持有需要释放的资源。在一个操作需要以通用的方式来结束,或者当你知道其实例需要释放资源时,建议实现AutoCloseable接口,并使用try-with-resources语法。
接着我们看下AutoCloseable.close方法上的Javadoc,以下是原文:
Closesthisresource,relinquishinganyunderlyingresources.Thismethodisinvokedautomaticallyonobjectsmanagedbythetry-with-resourcesstatement.
WhilethisinterfacemethodisdeclaredtothrowException,implementersarestronglyencouragedtodeclareconcreteimplementationsoftheclosemethodtothrowmorespecificexceptions,ortothrownoexceptionatallifthecloseoperationcannotfail.Caseswherethecloseoperationmayfailrequirecarefulattentionbyimplementers.Itisstronglyadvisedtorelinquishtheunderlyingresourcesandtointernallymarktheresourceasclosed,priortothrowingtheexception.Theclosemethodisunlikelytobeinvokedmorethanonceandsothisensuresthattheresourcesarereleasedinatimelymanner.Furthermoreitreducesproblemsthatcouldarisewhentheresourcewraps,oriswrapped,byanotherresource.
ImplementersofthisinterfacearealsostronglyadvisedtonothavetheclosemethodthrowInterruptedException.
Thisexceptioninteractswithathread'sinterruptedstatus,andruntimemisbehaviorislikelytooccurifanInterruptedExceptionissuppressed.
Moregenerally,ifitwouldcauseproblemsforanexceptiontobesuppressed,theAutoCloseable.closemethodshouldnotthrowit.
Notethatunlikethejava.io.Closeable#closeclosemethodofjava.io.Closeable,thisclosemethodisnotrequiredtobeidempotent.Inotherwords,callingthisclosemethodmorethanoncemayhavesomevisiblesideeffect,unlikeCloseable.closewhichisrequiredtohavenoeffectifcalledmorethanonce.
However,implementersofthisinterfacearestronglyencouragedtomaketheirclosemethodsidempotent.
翻译如下:
此方法用于关闭资源,放弃任何底层基础资源。当使用try-with-resources语法管理对象时,close方法将在try代码块逻辑结束后自动被调用。
虽然AutoCloseable中的close方法被声明为抛出Exception这个异常,但是强烈建议实现类声明更加具体的异常,或者当close操作不允许失败时声明为不抛出任何异常。实现者需要注意close方法操作失败的情况,强烈建议放弃底层资源,并在抛出异常前在内部将资源标注为不可用。close方法不太可能被反复调用,因此这样确保close被调用后资源被及时标志为释放。此外,这样还能减少资源被其他资源包装时可能出现的问题。
强烈建议实现者在实现close方法时不要抛出InterruptedException。InterruptedException与线程状态交互影响,如果处理不当,可能导致运行时错误。更一般的情况下,当一个异常的不当处理会导致题,AutoCloseable.close方法就不应该抛出这个异常。
与java.io.Closeable.close方法不同的是,AutoCloseable.close方法的调用不要求幂等。换句话说,多次调用AutoCloseable.close可能会产生一些可见的副作用,不像Closeable.close允许多次调用。然而,强烈建议实现者将close方法实现为幂等。
2、Closeable中的Javadoc
Closeable类上的Javadoc无额外有用信息,我们看下Closeable.close方法上的Javadoc:
Closesthisstreamandreleasesanysystemresourcesassociatedwithit.Ifthestreamisalreadyclosedtheninvokingthismethodhasnoeffect.
AsnotedinAutoCloseable#close(),caseswheretheclosemayfailrequirecarefulattention.ItisstronglyadvisedtorelinquishtheunderlyingresourcesandtointernallymarktheCloseableasclosed,priortothrowingtheIOException.
翻译如下:
此方法用于关闭流并释放与之相关的任何系统资源。
如果流已经关闭,则close方法的调用将无任何效果。如同AutoCloseable#close()的Javadoc描述的那样,需要关注关闭失败的情况。在抛出IOException异常前,强烈建议放弃底层资源并在内部标注资源已关闭。
四、揭开魔术面纱
1、怎么实现的
由于不确定try-with-resources是一种语法糖,还是在JVM虚拟机层面新加指令进行的支持,我尝试使用javap解析class文件后,通过解析结果查表对照发现,两段代码不同之处对应的字节码指令并不是此语法的含义,判定其确实是一种语法糖,在变成class文件的时候已经由编译器(javac)处理了。既然如此,我们就直接将其反编译,得到的结果如下:
publicstaticvoidf1(){ Resourceresource=newResource(); try{ resource.read(); }finally{ try{ resource.close(); }catch(Exceptionvar7){ } } }
publicstaticvoidf2(){ try{ Resourceresource=newResource(); Throwablevar1=null; try{ resource.read(); }catch(Throwablevar11){ var1=var11; throwvar11; }finally{ if(resource!=null){ if(var1!=null){ try{ resource.close(); }catch(Throwablevar10){ var1.addSuppressed(var10); } }else{ resource.close(); } } } }catch(Exceptionvar13){ } }
可以看到代码片段1与原代码几乎一样,但是代码片段2与原代码确实大相径庭,接着我们来分析下:
关注点1:newResource()操作被放到了try内部了,如果不用try-with-resources语法,我们一般放到try外面。
关注点2:resource.read()操作被捕获了Throwable,并通过赋值记录了下来。
关注点3:fianally中的if(resource!=null)这一步骤是多余的,因为只有在newResource()操作抛异常才会存在resource为空的情况,然而这个时候就不会执行到这里来了。
关注点4:此时一定要执行resource.close()了,当var1不为空(即resource.read()抛出了异常),则需捕获close可能抛出的异常,并调用var1.addSuppressed记录关联try中抛出的异常,我们后面再分析这步骤。
关注点5:由于在原始代码里我们捕获了close方法抛出的异常,因此这里当上一步的var1为空时可能抛出的异常需要在最外层捕获。
2、Throwable.addSuppressed
publicfinalsynchronizedvoidaddSuppressed(Throwableexception){ if(exception==this) thrownewIllegalArgumentException(SELF_SUPPRESSION_MESSAGE,exception); if(exception==null) thrownewNullPointerException(NULL_CAUSE_MESSAGE); if(suppressedExceptions==null)//Suppressedexceptionsnotrecorded return; if(suppressedExceptions==SUPPRESSED_SENTINEL) suppressedExceptions=newArrayList<>(1); suppressedExceptions.add(exception); }
这段代码我们只需要了解,每一个Throwable实例中有一个List类型的字段,本方法将入参中的异常添加到了这个List的末尾。
Appendsthespecifiedexceptiontotheexceptionsthatweresuppressedinordertodeliverthisexception.Thismethodisthread-safeandtypicallycalled(automaticallyandimplicitly)bythetry-with-resourcesstatement.
ThesuppressionbehaviorisenabledunlessdisabledThrowable(String,Throwable,boolean,boolean)viaaconstructor.Whensuppressionisdisabled,thismethoddoesnothingotherthantovalidateitsargument.Notethatwhenoneexceptioncausesanotherexception,thefirstexceptionisusuallycaughtandthenthesecondexceptionisthrowninresponse.Inotherwords,thereisacausalconnectionbetweenthetwoexceptions.Incontrast,therearesituationswheretwoindependentexceptionscanbethrowninsiblingcodeblocks,inparticularinthetryblockofatry-with-resourcesstatementandthecompiler-generatedfinallyblockwhichclosestheresource.Inthesesituations,onlyoneofthethrownexceptionscanbepropagated.Inthetry-with-resourcesstatement,whentherearetwosuchexceptions,theexceptionoriginatingfromthetryblockispropagatedandtheexceptionfromthefinallyblockisaddedtothelistofexceptionssuppressedbytheexceptionfromthetryblock.Asanexceptionunwindsthestack,itcanaccumulatemultiplesuppressedexceptions.
Anexceptionmayhavesuppressedexceptionswhilealsobeingcausedbyanotherexception.Whetherornotanexceptionhasacauseissemanticallyknownatthetimeofitscreation,unlikewhetherornotanexceptionwillsuppressotherexceptionswhichistypicallyonlydeterminedafteranexceptionisthrown.
Notethatprogrammerwrittencodeisalsoabletotakeadvantageofcallingthismethodinsituationswheretherearemultiplesiblingexceptionsandonlyonecanbepropagated.
翻译如下:
为了传递此异常(入参),将其附加到记录到this中的实例字段(List)中。这个方法是线程安全的,通常由try-with-resources语句(自动和隐式地)调用。除非通过Throwable(String,Throwable,boolean,boolean)构造方法来显示禁用,否则将启用这个行为。禁用后,此方法除了验证参数外不做其他任何事情。
注意,当一个异常引发另一个异常时,通常会捕获第一个异常,然后在响应中抛出第二个异常(就是抛出最上层的异常)。换句话说,这两个异常之间存在因果关系。当然也存在例外情况,可以在兄弟代码块中抛出两个独立的异常,特别是在try-with-resources语句的try块和编译器生成的finally块(用于关闭资源)中。在这些情况下,只能传播一个抛出的异常。在try-with-resources语句中,如果存在两个这样的异常,则传播源自try块的异常,并将来自finally块的异常记录到try代码块抛出的异常中。当你展开一个异常堆栈时,你可以看到它累积的多个记录的未抛出的异常(因为只能抛出一个异常,其他异常只能通过这个方式来记录)。一个异常可能记录有未抛出的异常,同时其自身也是由另一个异常引起的。一个异常是否由其他异常引起在创建时就已经在语义上知道了(即通过构造函数),这与这个异常是否通过Throwable.addSuppressed方式记录有异常不同,后者通常只在抛出异常后才能知道。
注意,程序员编写的代码,也能够在有多个兄弟异常(兄弟异常可以理解为相互并无关联)且只能传播一个异常的情况下,利用调用此方法的优势。
阅读完这个Javadoc注释后,建议结合Throwable类源码加深理解。
五、JDK9中的改进
通过搜索学习,还了解到在JDK9中对此语法进行了改进,JSR334对其进行了描述,感兴趣的可以点击 此处 或者 此处 来进一步了解优化点。
六、最佳实践
本次学习总结出如下最佳实践:
- 使用try-with-resources语法无论是否抛出异常在try-block执行完毕后都会调用资源的close方法。
- 使用try-with-resources语法创建多个资源,try-block执行完毕后调用的close方法的顺序与创建资源顺序相反。
- 使用try-with-resources语法,try-block块抛出异常后先执行所有资源(try的()中声明的)的close方法然后在执行catch里面的代码然后才是finally。
- try的()中管理的资源在构造时抛出的异常,需要在本try对应的catch中捕获。
- 自动调用的close方法显示声明的异常,需要在本try对应的catch中捕获。
- 当你的try-catch-finally分别在try/catch/finally中有异常抛出,而无法抉择抛出哪个异常时,你应该抛出try中对应的异常,并通过Throwable.addSuppressed方式记录它们间的关系。
以上就是详解Java中的AutoCloseable接口的详细内容,更多关于JavaAutoCloseable接口的资料请关注毛票票其它相关文章!