您的位置:首页 > 其它

Error Handling with Exceptions【5】

2007-11-21 12:38 405 查看
Therestrictiononexceptionsdoesnotapplytoconstructors.YoucanseeinStormyInningthataconstructorcanthrowanythingitwants,regardlessofwhatthebase-classconstructorthrows.However,sinceabase-classconstructormustalwaysbecalledonewayoranother(here,thedefaultconstructoriscalledautomatically),thederived-classconstructormustdeclareanybase-classconstructorexceptionsinitsexceptionspecification.Notethataderived-classconstructorcannotcatchexceptionsthrownbyitsbase-classconstructor.

对于异常的约束在构造方法上是无效的,你可以看到在StormyInning中它的构造方法可以抛出任何它想抛出的异常,不管它的基类的构造方法抛出什么异常信息。单位因为基类的构造方法总是会通过一种或者其它的方式被调用,这里默认的构造方法是自动调用的,所以派生类的构造方法必须声明它的基类构造方法抛出的异常。注意的是派生类的构造方法不能捕获它的基类的构造方法抛出的异常信息的。

ThereasonStormyInning.walk()willnotcompileisthatitthrowsanexception,butInning.walk()doesnot.Ifthiswereallowed,thenyoucouldwritecodethatcalledInning.walk()andthatdidn’thavetohandleanyexceptions,butthenwhenyousubstitutedanobjectofaclassderivedfromInning,exceptionswouldbethrownsoyourcodewouldbreak.Byforcingthederived-classmethodstoconformtotheexceptionspecificationsofthebase-classmethods,substitutabilityofobjectsismaintained.

StormyInning.walk()之所以不能编译通过是因为它会抛出异常,但是Inning.walk()并没有抛出异常。如果这被允许的话,当你写代码调用Inning.walk()的时候就不会作异常处理了,但是当你使用这个类的派生类调用的时候则会抛出异常,那么程序就终止了。通过强制派生类遵守基类的异常声明,这样对象的互换就可以得到保证。

Theoverriddenevent()methodshowsthataderived-classversionofamethodmaychoosenottothrowanyexceptions,evenifthebase-classversiondoes.Again,thisisfinesinceitdoesn’tbreakanycodethatiswritten—assumingthebase-classversionthrowsexceptions.SimilarlogicappliestoatBat(),whichthrowsPopFoul,anexceptionthatisderivedfromFoulthrownbythebase-classversionofatBat().Thisway,ifyouwritecodethatworkswithInningandcallsatBat(),youmustcatchtheFoulexception.SincePopFoulisderivedfromFoul,theexceptionhandlerwillalsocatchPopFoul.

而覆写的event()这个方法则表明派生类的某个方法可以选择不去抛出异常,甚至它的基类中这个方法抛出了异常。这是因为即使基类抛出异常也不会影响已有的程序。atBat()也是同样的道理。这个异常信息是继承的基类中atBat()方法抛出的异常。这样如果你使用Inning调用atBat()的话,你必须要捕获Foul异常。因为PopFoul是继承的Foul,这样异常处理程序同样也可以捕获PopFoul。

Thelastpointofinterestisinmain().Here,youcanseethatifyou’redealingwithexactlyaStormyInningobject,thecompilerforcesyoutocatchonlytheexceptionsthatarespecifictothatclass,butifyouupcasttothebasetype,thenthecompiler(correctly)forcesyoutocatchtheexceptionsforthebasetype.Alltheseconstraintsproducemuchmorerobustexception-handlingcode

我们最后的一个关注点就是main()方法了,这里你可以看到如果你处理的是一个StormyInning对象的话,编译器会强制要求你值捕获这个类抛出的异常,但是如果你上传转化为基类的对象,这时编译器就要求你捕获基类的异常信息。这些异常限制使得你的异常处理程序更加强壮。

It’susefultorealizethatalthoughexceptionspecificationsareenforcedbythecompilerduringinheritance,theexceptionspecificationsarenotpartofthetypeofamethod,whichcomprisesonlythemethodnameandargumenttypes.Therefore,youcannotoverloadmethodsbasedonexceptionspecifications.Inaddition,justbecauseanexceptionspecificationexistsinabase-classversionofamethoddoesn’tmeanthatitmustexistinthederived-classversionofthemethod.Thisisquitedifferentfrominheritancerules,whereamethodinthebaseclassmustalsoexistinthederivedclass.Putanotherway,the“exceptionspecificationinterface”foraparticularmethodmaynarrowduringinheritanceandoverriding,butitmaynotwiden—thisispreciselytheoppositeoftherulefortheclassinterfaceduringinheritance.

在继承中尽管编译器会对异常处理作出强制要求,异常处理并不是方法类型的一部分,它是由方法名称和参数类型组成的。因此你不能通过异常处理来实现重载。换一个说法来说,仅仅因为在基类中的方法中使用了异常处理程序就必须要在派生类中的这个方法抛出这个异常。这和继承的规则是有很大的不同的。换一种说法,异常处理程序接口通过继承和覆写会变得越来越小这和继承中接口的规则是正好相反的。

Constructors

Whenwritingcodewithexceptions,it’sparticularlyimportantthatyoualwaysask“Ifanexceptionoccurs,willthisbeproperlycleanedup?”Mostofthetimeyou’refairlysafe,butinconstructorsthere’saproblem.Theconstructorputstheobjectintoasafestartingstate,butitmightperformsomeoperation—suchasopeningafile—thatdoesn’tgetcleanedupuntiltheuserisfinishedwiththeobjectandcallsaspecialcleanupmethod.Ifyouthrowanexceptionfrominsideaconstructor,thesecleanupbehaviorsmightnotoccurproperly.Thismeansthatyoumustbeespeciallydiligentwhileyouwriteyourconstructor.

当你使用异常信息来编写程序的时候,你可能会问“如果发生异常的话会被合理的清理掉吗”,这个问题很重要,大部分时间来看都是安全的,但是构造方法是个问题。构造方法把对象放到一个安全的起始状态,但是它可能会执行一些操作-例如打开文件-直到操作员对这个对象的操作结束了并且调用一个特殊的清理的方法之前它是不会得到清理的。如果你在构造方法内抛出了一个异常,那这些清理的方法就可能得不到正常的执行了。

Sinceyou’vejustlearnedaboutfinally,youmightthinkthatitisthecorrectsolution.Butit’snotquitethatsimple,becausefinallyperformsthecleanupcodeeverytime,eveninthesituationsinwhichyoudon’twantthecleanupcodeexecuteduntilthecleanupmethodruns.Thus,ifyoudoperformcleanupinfinally,youmustsetsomekindofflagwhentheconstructorfinishesnormallysothatyoudon’tdoanythinginthefinallyblockiftheflagisset.Becausethisisn’tparticularlyelegant(youarecouplingyourcodefromoneplacetoanother),it’sbestifyoutrytoavoidperformingthiskindofcleanupinfinallyunlessyouareforcedto.

因为你刚刚学习了finally,所以你可能想这是正确的解决办法。但是远远不是那么简单,因为finally每次都会调用cleanup方法,甚至于在你不希望执行的时候它也会执行。因此,如果你使用finally来执行清理的话,你必须要在构造方法正常执行结束之后设置一个标志位,这样的话你就可以根据标志位的值在finally中什么也不作。但是这不是明智的做法,这样造成你的代码之间的耦合增高。所以如果不是必须的话要尽量的去避免使用finally来执行清理工作。

Inthefollowingexample,aclasscalledInputFileiscreatedthatopensafileandallowsyoutoreaditoneline(convertedintoaString)atatime.ItusestheclassesFileReaderandBufferedReaderfromtheJavastandardI/OlibrarythatwillbediscussedinChapter12,butwhicharesimpleenoughthatyouprobablywon’thaveanytroubleunderstandingtheirbasicuse:

在接下来的例子中,一个InputFile的类来打开一个文件并且读取一行转化为String型,使用了Java标准I/O库中建立了FileReader和BufferedReader,这些将会在第12章中讲解到,但是很简单,不会影响你理解它们的基本的使用。

//:c09:Cleanup.java

//Payingattentiontoexceptionsinconstructors.

importcom.bruceeckel.simpletest.*;

importjava.io.*;


classInputFile{

privateBufferedReaderin;

publicInputFile(Stringfname)throwsException{

try{

in=newBufferedReader(newFileReader(fname));

//Othercodethatmightthrowexceptions

}catch(FileNotFoundExceptione){

System.err.println("Couldnotopen"+fname);

//Wasn'topen,sodon'tcloseit

throwe;

}catch(Exceptione){

//Allotherexceptionsmustcloseit

try{

in.close();

}catch(IOExceptione2){

System.err.println("in.close()unsuccessful");

}

throwe;//Rethrow

}finally{

//Don'tcloseithere!!!

}

}

publicStringgetLine(){

Strings;

try{

s=in.readLine();

}catch(IOExceptione){

thrownewRuntimeException("readLine()failed");

}

returns;

}

publicvoiddispose(){

try{

in.close();

System.out.println("dispose()successful");

}catch(IOExceptione2){

thrownewRuntimeException("in.close()failed");

}

}

}



publicclassCleanup{

privatestaticTestmonitor=newTest();

publicstaticvoidmain(String[]args){

try{

InputFilein=newInputFile("Cleanup.java");

Strings;

inti=1;

while((s=in.getLine())!=null)

;//Performline-by-lineprocessinghere...

in.dispose();

}catch(Exceptione){

System.err.println("CaughtExceptioninmain");

e.printStackTrace();

}

monitor.expect(newString[]{

"dispose()successful"

});

}

}

TheconstructorforInputFiletakesaStringargument,whichisthenameofthefileyouwanttoopen.Insideatryblock,itcreatesaFileReaderusingthefilename.AFileReaderisn’tparticularlyusefuluntilyouturnaroundanduseittocreateaBufferedReaderthatyoucanactuallytalkto—noticethatoneofthebenefitsofInputFileisthatitcombinesthesetwoactions.

InputFile类的构造方法使用了一个字符串作为参数,该参数意义为你希望打开的文件名。在Try中使用参数创建了一个FileReader类。FileReader并不是很有用,你只是用它去创建一个BufferedReader,而InputFile做的好的地方就是把这两步结合起来了。

IftheFileReaderconstructorisunsuccessful,itthrowsaFileNotFoundException,whichmustbecaughtseparately.Thisistheonecaseinwhichyoudon’twanttoclosethefile,becauseitwasn’tsuccessfullyopened.Anyothercatchclausesmustclosethefilebecauseitwasopenedbythetimethosecatchclausesareentered.(Ofcourse,thisistrickierifmorethanonemethodcanthrowaFileNotFoundException.Inthatcase,youmightwanttobreakthingsintoseveraltryblocks.)Theclose()methodmightthrowanexceptionsoitistriedandcaughteventhoughit’swithintheblockofanothercatchclause—it’sjustanotherpairofcurlybracestotheJavacompiler.Afterperforminglocaloperations,theexceptionisrethrown,whichisappropriatebecausethisconstructorfailed,andyouwouldn’twantthecallingmethodtoassumethattheobjecthadbeenproperlycreatedandwasvalid.

如果FileReader的构造方法没有成功,它会抛出一个FileNotFoundException的异常信息,这个是必须要捕获的异常信息。这是其中的一种情况因为文件并没有成功的打开所以不需要关闭这个文件。而其它的捕获异常中则必需要关闭这个文件,因为进入到其它的异常中的时候这个文件已经被打开了。(当然如果还有其它的方法也抛出这个异常信息的话这就有点投机取巧的意思了,如果是那种情况的话,你可能会想把代码拆分到几个Try中去)。Close()方法可能也会抛出异常信息所以也对其进行了try-catch操作。尽管它们在另外一个try-catch的catch模块中,那对于编译器而言只是一对花括号而已。经过这层操作之后,异常再次被抛出,这是很合理的,因为构造方法失败了,而你又不希望这个方法被误认为这个对象成功的创建了并且是有效的吧。

Inthisexample,whichdoesn’tusetheaforementionedflaggingtechnique,thefinallyclauseisdefinitelynottheplacetoclose()thefile,sincethatwouldcloseiteverytimetheconstructorcompleted.BecausewewantthefiletobeopenfortheusefullifetimeoftheInputFileobject,thiswouldnotbeappropriate.

在上面这个例子中,并没有使用我们前面提到的标志位的技术,因为finally语句中并不是关闭这个文件的位置。因为那样的话它会每次在构造方法执行完毕后调用。因为我们希望在InputFile对象的整个声明周期这个文件一直是打开的,这么做显然并不是合适的。

ThegetLine()methodreturnsaStringcontainingthenextlineinthefile.ItcallsreadLine(),whichcanthrowanexception,butthatexceptioniscaughtsogetLine()doesn’tthrowanyexceptions.Oneofthedesignissueswithexceptionsiswhethertohandleanexceptioncompletelyatthislevel,tohandleitpartiallyandpassthesameexception(oradifferentone)on,orwhethertosimplypassiton.Passingiton,whenappropriate,cancertainlysimplifycoding.Inthissituation,thegetLine()methodconvertstheexceptiontoaRuntimeExceptiontoindicateaprogrammingerror.

getLine()方法返回了一个字符串,它包含了文件中的一行。它调用了readLine(),这个方法可能会抛出异常,但是这个异常已经被捕获了所以getLine()不需要再抛出异常。关于异常的一个设计的概念就是将异常信息在这一层抛出,并且将同样的或者不同的异常再次抛出,或者直接抛出,如果合理操作的话可以在一定程度上简化代码。在这种情况下getLine()方法将异常信息转化为了RuntimeException来标识程序错误。

Thedispose()methodmustbecalledbytheuserwhenfinishedusingtheInputFileobject.Thiswillreleasethesystemresources(suchasfilehandles)thatareusedbytheBufferedReaderand/orFileReaderobjects.Youdon’twanttodothisuntilyou’refinishedwiththeInputFileobject,atthepointyou’regoingtoletitgo.Youmightthinkofputtingsuchfunctionalityintoafinalize()method,butasmentionedinChapter4,youcan’talwaysbesurethatfinalize()willbecalled(evenifyoucanbesurethatitwillbecalled,youdon’tknowwhen).ThisisoneofthedownsidestoJava;Allcleanup—otherthanmemorycleanup—doesn’thappenautomatically,soyoumustinformtheclientprogrammerthattheyareresponsible,andpossiblyguaranteethatcleanupoccursusingfinalize().

当InputFile对象使用完毕之后用户必需要调用dispose()方法,这个方法会释放BufferedReader或者FileReader使用的系统资源例如文件句柄,在你使用完毕InputFile对象之前你不需要作这些。你可能在想将这些方法放入到finalize()方法中,但是在第四章中我们提到过你不能总是认为finalize()这个方法会被调用(就算你确信它会被调用,但你也不知道何时被调用)。这就是Java的另一面,除了内存清理之外的清理工作都不是自动发生的,所以你必需要告诉客户端程序员他们有责任而且必需保证在使用finalize()方法的时候必须要调用清理的动作。

InCleanup.javaanInputFileiscreatedtoopenthesamesourcefilethatcreatestheprogram,thefileisreadinalineatatime,andlinenumbersareadded.Allexceptionsarecaughtgenericallyinmain(),althoughyoucouldchoosegreatergranularity.

在Cleanup.java中InputFile被创建来打开创建这个程序的源码,这个文件被读取一行,并且加上了行号。虽然你可以选择更加细致的方法,但是所有的异常信息还是在main()方法被捕获的。

Oneofthebenefitsofthisexampleistoshowyouwhyexceptionsareintroducedatthispointinthebook—therearemanylibraries(likeI/O,mentionedearlier)thatyoucan’tusewithoutdealingwithexceptions.ExceptionsaresointegraltoprogramminginJava,especiallybecausethecompilerenforcesthem,thatyoucanaccomplishonlysomuchwithoutknowinghowtoworkwiththem.

这里例子中的一个优点就是告诉你为什么要介绍异常,在本书中涉及到的很多类库如果你不处理异常的话根本没办法使用。异常信息是Java中密不可分的,特别是编译器也对这些有强制的要求,如果你对异常信息一点都不了解的话那么也只能到此为止了。

Exceptionmatching

Whenanexceptionisthrown,theexceptionhandlingsystemlooksthroughthe“nearest”handlersintheordertheyarewritten.Whenitfindsamatch,theexceptionisconsideredhandled,andnofurthersearchingoccurs.

当一个异常信息被抛出的时候,异常处理程序会按照它们书写的顺序先查找距离最近的异常处理程序。当找到匹配的后,异常就被认为处理了,其它的就不再继续寻找了。

Matchinganexceptiondoesn’trequireaperfectmatchbetweentheexceptionanditshandler.Aderived-classobjectwillmatchahandlerforthebaseclass,asshowninthisexample:

异常信息的匹配并不需要将异常和它的异常处理程序中的异常完全的匹配,派生类的对象和基类的对象是可以匹配的,就像下面展现的例子一样。

//:c09:Human.java

//Catchingexceptionhierarchies.

importcom.bruceeckel.simpletest.*;


classAnnoyanceextendsException{}

classSneezeextendsAnnoyance{}


publicclassHuman{

privatestaticTestmonitor=newTest();

publicstaticvoidmain(String[]args){

try{

thrownewSneeze();

}catch(Sneezes){

System.err.println("CaughtSneeze");

}catch(Annoyancea){

System.err.println("CaughtAnnoyance");

}

monitor.expect(newString[]{

"CaughtSneeze"

});

}

}


TheSneezeexceptionwillbecaughtbythefirstcatchclausethatitmatches,whichisthefirstone,ofcourse.However,ifyouremovethefirstcatchclause,leavingonly:

因为匹配了第一个异常,Sneeze异常信息会被捕获,这是第一个异常,但是如果删除这个异常匹配语句的话,只是剩下

try{

thrownewSneeze();

}catch(Annoyancea){

System.err.println("CaughtAnnoyance");

}

thecodewillstillworkbecauseit’scatchingthebaseclassofSneeze.Putanotherway,catch(Annoyancee)willcatchanAnnoyanceoranyclassderivedfromit.Thisisusefulbecauseifyoudecidetoaddmorederivedexceptionstoamethod,thentheclientprogrammer’scodewillnotneedchangingaslongastheclientcatchesthebaseclassexceptions.

程序仍然会继续运行,因为它可以捕获Sneeze的基类异常,换一种说法,捕获Annoyance异常的异常处理程序会捕获Annoyance或者它的任何一个派生类的异常。这是很有用的,因为你可以为这个方法增加更多的派生类异常信息,这样如果客户端程序捕获基类的异常信息的话代码就不需要作任何修改了。

Ifyoutryto“mask”thederived-classexceptionsbyputtingthebase-classcatchclausefirst,likethis:

如果你试图将基类的异常捕获语句放置在最前面,来捕获它的派生类的异常的话,像这样:

try{

thrownewSneeze();

}catch(Annoyancea){

System.err.println("CaughtAnnoyance");

}catch(Sneezes){

System.err.println("CaughtSneeze");

}


thecompilerwillgiveyouanerrormessage,sinceitseesthattheSneezecatch-clausecanneverbereached.

这样编译器会提示一个错误信息,因为它认为Sneeze的异常处理程序永远不会被执行。

Alternativeapproaches

Anexception-handlingsystemisatrapdoorthatallowsyourprogramtoabandonexecutionofthenormalsequenceofstatements.Thetrapdoorisusedwhenan“exceptionalcondition”occurs,suchthatnormalexecutionisnolongerpossibleordesirable.Exceptionsrepresentconditionsthatthecurrentmethodisunabletohandle.Thereasonexceptionhandlingsystemsweredevelopedisbecausetheapproachofdealingwitheachpossibleerrorconditionproducedbyeachfunctioncallwastooonerous,andprogrammerssimplyweren’tdoingit.Asaresult,theywereignoringtheerrors.It’sworthobservingthattheissueofprogrammerconvenienceinhandlingerrorswasaprimemotivationforexceptionsinthefirstplace.

异常处理程序是允许你不按照正常的顺序的一扇暗门。这经常被用来当异常发生的时候,正常的执行的话已经是不可能或者不合理的了。异常描述的是这些方法已经不能继续处理了,而异常处理程序产生的原因就是对每个方法调用都作错误检查实在是太费力了,而程序员也不这么作,而最后的结果就是他们忽略了这些错误信息。需要注意的是异常处理程序产生的初衷就是为了简化程序员的工作。

Oneoftheimportantguidelinesinexceptionhandlingis“don’tcatchanexceptionunlessyouknowwhattodowithit.”Infact,oneoftheimportantgoalsofexceptionhandlingistomovetheerror-handlingcodeawayfromthepointwheretheerrorsoccur.Thisallowsyoutofocusonwhatyouwanttoaccomplishinonesectionofyourcode,andhowyou’regoingtodealwithproblemsinadistinctseparatesectionofyourcode.Asaresult,yourmainlinecodeisnotclutteredwitherror-handlinglogic,andit’smucheasiertounderstandandmaintain.

异常处理程序的最重要的一个原则就是:除非你知道你要对这个异常作什么否则的话就不要捕获这个异常信息。事实上,异常处理程序最重要的一个目的就是异常处理部分的代码与异常发生地分离开来,这样的话你可以在某个位置将工作的重点集中到这里要完成什么工作,而处理处理这个异常信息解决部分则放到另外一个地方。结果就是你代码不会因为异常处理逻辑而混乱,也很容易理解和维护。

Checkedexceptionscomplicatethisscenarioabit,becausetheyforceyoutoaddcatchclausesinplaceswhereyoumaynotbereadytohandleanerror.Thisresultsinthe“harmfulifswallowed”problem:

Checkedexceptions使得程序复杂了一些,因为要求即使你没准备好捕获错误异常,也强制你增加catch语句,那么这么异常可能被私吞了。

try{

//...todosomethinguseful

}catch(ObligatoryExceptione){}//Gulp!

Programmers(myselfincluded,inthefirsteditionofthisbook)wouldjustdothesimplestthing,andswallowtheexception—oftenunintentionally,butonceyoudoit,thecompilerhasbeensatisfied,sounlessyouremembertorevisitandcorrectthecode,theexceptionwillbelost.Theexceptionhappens,butitvanishescompletelywhenswallowed.Becausethecompilerforcesyoutowritecoderightawaytohandletheexception,thisseemsliketheeasiestsolutioneventhoughit’sprobablytheworstthingyoucando.

程序员们都会作很简单的东西,私吞这些异常信息但是经常不是故意这么作的,但是一旦这么作了,编译器是不会提示问题的,所以除非你记得回过头来修改完善这段代码,否则这个异常信息将被丢掉。这个异常发生了,但是当被私吞是就消失的无影无踪了。因为编译器要求你必须编写代码来处理这个异常信息,尽管这是最坏的办法但是这看起来确实是最简单的办法了。

HorrifieduponrealizingthatIhaddonethis,inthesecondeditionI“fixed”theproblembyprintingthestacktraceinsidethehandler(asisstillseen—appropriately—inanumberofexamplesinthischapter).Whilethisisusefultotracethebehaviorofexceptions,itstillindicatesthatyoudon’treallyknowwhattodowiththeexceptionatthatpointinyourcode.Inthissectionwe’lllookatsomeoftheissuesandcomplicationsarisingfromcheckedexceptions,andoptionsthatyouhavewhendealingwiththem.

当我意识到我犯了一个错误的时候我吓了一大跳,在第二个版本中我在异常处理程序中打印堆栈的轨迹来修复了这个问题,本章中还是有很多的例子采用了这个办法这是很合理的,它可以很好的记录异常发生的行为轨迹,但是你可能还是不知道应该如何处理这个异常。在本小节中你可以来看看checkedexception所带来的并发症以及问题,以及通过什么方法来解决这个问题。

Thistopicseemssimple.Butitisnotonlycomplicated,itisalsoanissueofsomevolatility.Therearepeoplewhoarestaunchlyrootedoneithersideofthefenceandwhofeelthatthecorrectanswer(theirs)isblatantlyobvious.Ibelievethereasonforoneofthesepositionsisthedistinctbenefitseeningoingfromapoorly-typedlanguagelikepre-ANSICtoastrong,statically-typedlanguage(thatis,checkedatcompile-time)likeC++orJava.Whenyoumakethattransition(asIdid),thebenefitsaresodramaticthatitcanseemlikestrongstatictypecheckingisalwaysthebestanswertomostproblems.Myhopeistorelatealittlebitofmyownevolution,thathasbroughttheabsolutevalueofstrongstatictypecheckingintoquestion;clearly,it’sveryhelpfulmuchofthetime,butthere’safuzzylinewecrosswhenitbeginstogetinthewayandbecomeahindrance(oneofmyfavoritequotesis:“Allmodelsarewrong.Someareuseful.”).

这个话题看起来很简单,但实际上还不仅仅是复杂,更重要的是它还非常多变。总有人会顽固的坚持自己的立场,声明正确的答案是不言自明的。我觉得之所以有这种论点,是因为我们所使用的工具已经不是“ANSI标准出台前C那样的弱类型语言了”,而是像C++和JAVA这样强静态类型语言,也就是编译时就作类型检查的语言,这是前者所无法比你的。当你港开始这个转变的时候,会发现它带来的好处是那样生动,好像类型检查能一劳永逸解决所有的问题。这里,我想结合自己的认识过程,告诉你我是怎样从对类型检查的绝对迷信变成怀疑的;当然,很多的时候还是非常有用的,但是当它阻挡了我们的去路并且称为的时候,我们就得跨过去。只是这条界限并不是很清晰,我最喜欢的一句格言就是“所有的模型都是错误的,但是有些是能用的”。

History

ExceptionhandlingoriginatedinsystemslikePL/1andMesa,andlaterappearedinCLU,Smalltalk,Modula-3,Ada,Eiffel,C++,Python,Java,andthepost-JavalanguagesRubyandC#.TheJavadesignissimilartoC++,exceptinplaceswheretheJavadesignersfeltthattheC++designcausedproblems.

Toprovideprogrammerswithaframeworkthattheyweremorelikelytouseforerrorhandlingandrecovery,exceptionhandlingwasaddedtoC++ratherlateinthestandardizationprocess,promotedbyBjarneStroustrup,thelanguage’soriginalauthor.ThemodelforC++exceptionscameprimarilyfromCLU.However,otherlanguagesexistedatthattimethatalsosupportedexceptionhandling:Ada,Smalltalk(bothofwhichhadexceptionsbutnoexceptionspecifications)andModula-3(whichincludedbothexceptionsandspecifications).

Intheirseminalpaper[44]onthesubject,LiskovandSnydernotethatamajordefectoflanguageslikeCthatreporterrorsinatransientfashionisthat:

“...everyinvocationmustbefollowedbyaconditionaltesttodeterminewhattheoutcomewas.Thisrequirementleadstoprogramsthataredifficulttoread,andprobablyinefficientaswell,thusdiscouragingprogrammersfromsignalingandhandlingexceptions.”

Notethatoneoftheoriginalmotivationsofexceptionhandlingwastopreventthisrequirement,butwithcheckedexceptionsinJavawecommonlyseeexactlythiskindofcode.Theygoontosay:

“...requiringthatthetextofahandlerbeattachedtotheinvocationthatraisestheexceptionwouldleadtounreadableprogramsinwhichexpressionswerebrokenupwithhandlers.”

FollowingtheCLUapproachwhendesigningC++exceptions,Stroustrupstatedthatthegoalwastoreducetheamountofcoderequiredtorecoverfromerrors.Ibelievethathewasobservingthatprogrammersweretypicallynotwritingerror-handlingcodeinCbecausetheamountandplacementofsuchcodewasdauntinganddistracting.Asaresult,theywereusedtodoingittheCway,ignoringerrorsincodeandusingdebuggerstotrackdownproblems.Touseexceptions,theseCprogrammershadtobeconvincedtowrite“additional”codethattheyweren’tnormallywriting.Thus,todrawthemintoabetterwayofhandlingerrors,theamountofcodetheywouldneedto“add”mustnotbeonerous.Ithinkit’simportanttokeepthisgoalinmindwhenlookingattheeffectsofcheckedexceptionsinJava.

C++broughtanadditionalideaoverfromCLU:theexceptionspecification,toprogrammaticallystateinthemethodsignaturewhatexceptionsmayresultfromcallingthatmethod.Theexceptionspecificationreallyhastwopurposes.Itcansay“I’moriginatingthisexceptioninmycode,youhandleit.”Butitcanalsomean“I’mignoringthisexceptionthatcanoccurasaresultofmycode,youhandleit.”We’vebeenfocusingonthe“youhandleit”partwhenlookingatthemechanicsandsyntaxofexceptions,buthereI’mparticularlyinterestedinthefactthatoftenweignoreexceptionsandthat’swhattheexceptionspecificationcanstate.

InC++theexceptionspecificationisnotpartofthetypeinformationofafunction.Theonlycompile-timecheckingistoensurethatexceptionspecificationsareusedconsistently;forexample,ifafunctionormethodthrowsexceptions,thentheoverloadedorderivedversionsmustalsothrowthoseexceptions.UnlikeJava,however,nocompile-timecheckingoccurstodeterminewhetherornotthefunctionormethodwillactuallythrowthatexception,orwhethertheexceptionspecificationiscomplete(thatis,whetheritaccuratelydescribesallexceptionsthatmaybethrown).Thatvalidationdoeshappen,butonlyatruntime.Ifanexceptionisthrownthatviolatestheexceptionspecification,theC++programwillcallthestandardlibraryfunctionunexpected().

Itisinterestingtonotethat,becauseoftheuseoftemplates,exceptionspecificationsarenotusedatallinthestandardC++library.Exceptionspecifications,then,mayhaveasignificantimpactonthedesignofJavagenerics(Java’sversionofC++templates,expectedtoappearinJDK1.5).

Perspectives

First,it’sworthnotingthatJavaeffectivelyinventedthecheckedexception(clearlyinspiredbyC++exceptionspecificationsandthefactthatC++programmerstypicallydon’tbotherwiththem).Ithasbeenanexperiment,whichnolanguagesincehaschosentoduplicate.

Secondly,checkedexceptionsappeartobeanobviousgoodthingwhenseeninintroductoryexamplesandinsmallprograms.Ithasbeensuggestedthatthesubtledifficultiesbegintoappearwhenprogramsstarttogetlarge.Ofcourse,largenessusuallydoesn’thappenovernight;itcreeps.Languagesthatmaynotbesuitedforlarge-scaleprojectsareusedforsmallprojectsthatgrow,andatsomepointwerealizethatthingshavegonefrommanageabletodifficult.ThisiswhatI’msuggestingmaybethecasewithtoomuchtypechecking;inparticular,withcheckedexceptions.

Thescaleoftheprogramseemstobeasignificantissue.Thisisaproblembecausemostdiscussionstendtousesmallprogramsasdemonstrations.OneoftheC#designersobservedthat:

“Examinationofsmallprogramsleadstotheconclusionthatrequiringexceptionspecificationscouldbothenhancedeveloperproductivityandenhancecodequality,butexperiencewithlargesoftwareprojectssuggestsadifferentresult—decreasedproductivityandlittleornoincreaseincodequality.”[45]

Inreferencetouncaughtexceptions,theCLUcreatorsstated:

“Wefeltitwasunrealistictorequiretheprogrammertoprovidehandlersinsituationswherenomeaningfulactioncanbetaken.”[46]

Whenexplainingwhyafunctiondeclarationwithnospecificationmeansthatitcanthrowanyexception,ratherthannoexceptions,Stroustrupstates:

“However,thatwouldrequireexceptionspecificationsforessentiallyeveryfunction,wouldbeasignificantcauseforrecompilation,andwouldinhibitcooperationwithsoftwarewritteninotherlanguages.Thiswouldencourageprogrammerstosubverttheexception-handlingmechanismsandtowritespuriouscodetosuppressexceptions.Itwouldprovideafalsesenseofsecuritytopeoplewhofailedtonoticetheexception.”[47]

Weseethisverybehavior—subvertingtheexceptions—happeningwithcheckedexceptionsinJava.

MartinFowler(authorofUMLDistilled,Refactoring,andAnalysisPatterns)wrotethefollowingtome:

“...onthewholeIthinkthatexceptionsaregood,butJavacheckedexceptionsaremoretroublethantheyareworth.”

InowthinkthatJava’simportantstepwastounifytheerrorreportingmodel,sothatallerrorsarereportedusingexceptions.Thiswasn’thappeningwithC++,becauseforbackwardcompatibilitywithCtheoldmodelofjustignoringerrorswasstillavailable.Butifyouhaveconsistentreportingwithexceptions,thentheexceptionscanbeusedifdesired,andifnot,theywillpropagateouttothehighestlevel(theconsoleorothercontainerprogram).WhenJavachangedtheC++modelsothatexceptionsweretheonlywaytoreporterrors,theextraenforcementofcheckedexceptionsmayhavebecomelessnecessary.

Inthepast,Ihavebeenastrongbelieverthatbothcheckedexceptionsandstrongstatictypecheckingwereessentialtorobustprogramdevelopment.However,bothanecdotalanddirectexperience[48]withlanguagesthataremoredynamicthanstatichaveleadmetothinkthatthegreatbenefitsactuallycomefrom:

Aunifiederror-reportingmodelviaexceptions,regardlessofwhethertheprogrammerisforcedbythecompilertohandlethem.

Typechecking,regardlessofwhenittakesplace.Thatis,aslongasproperuseofatypeisenforced,itdoesn’tmatterifithappensatcompiletimeorruntime.

Ontopofthis,thereareverysignificantproductivitybenefitstoreducingthecompile-timeconstraintsupontheprogrammer.Indeed,reflection(andeventually,generics)isrequiredtocompensatefortheover-constrainingnatureofstrongstatictyping,asyoushallseeinthenextchapterandinanumberofexamplesthroughoutthebook.

I’vealreadybeentoldbysomethatwhatIsayhereconstitutesblasphemy,andbyutteringthesewordsmyreputationwillbedestroyed,civilizationswillfall,andahigherpercentageofprogrammingprojectswillfail.Thebeliefthatthecompilercansaveyourprojectbypointingouterrorsatcompiletimerunsstrong,butit’sevenmoreimportanttorealizethelimitationofwhatthecompilerisabletodo;inChapter15,Iemphasizethevalueofanautomatedbuildprocessandunittesting,whichgiveyoufarmoreleveragethanyougetbytryingtoturneverythingintoasyntaxerror.It’sworthkeepinginmindthat:

Agoodprogramminglanguageisonethathelpsprogrammerswritegoodprograms.Noprogramminglanguagewillpreventitsusersfromwritingbadprograms.[49]

Inanyevent,thelikelihoodofcheckedexceptionseverbeingremovedfromJavaseemsdim.Itwouldbetooradicalofalanguagechange,andproponentswithinSunappeartobequitestrong.Sunhasahistoryandpolicyofabsolutebackwardscompatibility—togiveyouasenseofthis,virtuallyallSunsoftwarerunsonallSunhardware,nomatterhowold.However,ifyoufindthatsomecheckedexceptionsaregettinginyourway,orespeciallyifyoufindyourselfbeingforcedtocatchexceptions,butyoudon’tknowwhattodowiththem,therearesomealternatives.

Passingexceptionstotheconsole

Insimpleprograms,likemanyofthoseinthisbook,theeasiestwaytopreservetheexceptionswithoutwritingalotofcodeistopassthemoutofmain()totheconsole.Forexample,ifyouwanttoopenafileforreading(somethingyou’lllearnaboutindetailinChapter12),youmustopenandcloseaFileInputStream,whichthrowsexceptions.Forasimpleprogram,youcandothis(you’llseethisapproachusedinnumerousplacesthroughoutthisbook):

在简单的程序中,就像本书中很多的例子,最简单的办法而又不用写太多的代码来阻止异常就是将他们送到main()方法,送到输出台。举例来说,如果你想打开一个文件来读取,那么你就必须打开并且关闭FileInputStream对象,它会抛出异常,一个简单的例子你可以这么作:

//:c09:MainException.java

importjava.io.*;


publicclassMainException{

//Passallexceptionstotheconsole:

publicstaticvoidmain(String[]args)throwsException{

//Openthefile:

FileInputStreamfile=

newFileInputStream("MainException.java");

//Usethefile...

//Closethefile:

file.close();

}

}///:~


Notethatmain()isalsoamethodthatmayhaveanexceptionspecification,andherethetypeofexceptionisException,therootclassofallcheckedexceptions.Bypassingitouttotheconsole,youarerelievedfromwritingtry-catchclauseswithinthebodyofmain().(Unfortunately,fileI/Oissignificantlymorecomplexthanitwouldappeartobefromthisexample,sodon’tgettooexciteduntilafteryou’vereadChapter12).

需要说明的main()方法同样也是一个方法,也可以抛出异常信息,而这里异常抛出的是Exception的对象,所有异常信息的基类。通过将它输出到控制台,你就无需在main()方法中再写try-catch语句了。

Convertingcheckedtouncheckedexceptions

Throwinganexceptionfrommain()isconvenientwhenyou’rewritingamain(),butnotgenerallyuseful.Therealproblemiswhenyouarewritinganordinarymethodbody,andyoucallanothermethodandrealize“Ihavenoideawhattodowiththisexceptionhere,butIdon’twanttoswallowitorprintsomebanalmessage.”WithJDK1.4chainedexceptions,anewandsimplesolutionpreventsitself.Yousimply“wrap”acheckedexceptioninsideaRuntimeException,likethis:

当你在写main()方法的时候,在该方法中抛出异常是很简单的,但是一般讲没有太大的用处,当你书写代码的时候遇到的问题就是,你会调用一个其它的方法并且意识到如果这里发生异常的话我不知道该怎么做,但是我不想私吞这个异常,也不想打印出一些通俗的错误信息。在JDK1.4中一个新的简单的方法解决了,你直接把checkedexception放到RuntimeException中去,像下面这样:

try{

//...todosomethinguseful

}catch(IDontKnowWhatToDoWithThisCheckedExceptione){

thrownewRuntimeException(e);

}


Thisseemstobeanidealsolutionifyouwantto“turnoff”thecheckedexception—youdon’tswallowit,andyoudon’thavetoputitinyourmethod’sexceptionspecification,butbecauseofexceptionchainingyoudon’tloseanyinformationfromtheoriginalexception.

如果你想把checkedexception关闭的话看起来像是个不错的办法,不会私吞异常,也不必把它放到方法的异常说明里面,因为异常链你也不会丢失任何原始异常的信息。

Thistechniqueprovidestheoptiontoignoretheexceptionandletitbubbleupthecallstackwithoutbeingrequiredtowritetry-catchclausesand/orexceptionspecifications.However,youmaystillcatchandhandlethespecificexceptionbyusinggetCause(),asseenhere:

下面这种技术提供了一种办法来忽略遗产并且不需要写try-catch语句也不需要异常声明但是你仍然可以通过使用getCause()捕获这个异常。

//:c09:TurnOffChecking.java

//"Turningoff"Checkedexceptions.

importcom.bruceeckel.simpletest.*;

importjava.io.*;


classWrapCheckedException{

voidthrowRuntimeException(inttype){

try{

switch(type){

case0:thrownewFileNotFoundException();

case1:thrownewIOException();

case2:thrownewRuntimeException("WhereamI?");

default:return;

}

}catch(Exceptione){//Adapttounchecked:

thrownewRuntimeException(e);

}

}

}


classSomeOtherExceptionextendsException{}


publicclassTurnOffChecking{

privatestaticTestmonitor=newTest();

publicstaticvoidmain(String[]args){

WrapCheckedExceptionwce=newWrapCheckedException();

//Youcancallf()withoutatryblock,andlet

//RuntimeExceptionsgooutofthemethod:

wce.throwRuntimeException(3);

//Oryoucanchoosetocatchexceptions:

for(inti=0;i<4;i++)

try{

if(i<3)

wce.throwRuntimeException(i);

else

thrownewSomeOtherException();

}catch(SomeOtherExceptione){

System.out.println("SomeOtherException:"+e);

}catch(RuntimeExceptionre){

try{

throwre.getCause();

}catch(FileNotFoundExceptione){

System.out.println(

"FileNotFoundException:"+e);

}catch(IOExceptione){

System.out.println("IOException:"+e);

}catch(Throwablee){

System.out.println("Throwable:"+e);

}

}

monitor.expect(newString[]{

"FileNotFoundException:"+

"java.io.FileNotFoundException",

"IOException:java.io.IOException",

"Throwable:java.lang.RuntimeException:WhereamI?",

"SomeOtherException:SomeOtherException"

});

}

}///:~


WrapCheckedException.throwRuntimeException()containscodethatgeneratesdifferenttypesofexceptions.ThesearecaughtandwrappedinsideRuntimeExceptionobjects,sotheybecomethe“cause”ofthoseexceptions.

InTurnOffChecking,youcanseethatit’spossibletocallthrowRuntimeException()withnotryblockbecausethemethoddoesnotthrowanycheckedexceptions.However,whenyou’rereadytocatchexceptions,youstillhavetheabilitytocatchanyexceptionyouwantbyputtingyourcodeinsideatryblock.Youstartbycatchingalltheexceptionsyouexplicitlyknowmightemergefromthecodeinyourtryblock—inthiscase,SomeOtherExceptioniscaughtfirst.Lastly,youcatchRuntimeExceptionandthrowtheresultofgetCause()(thewrappedexception).Thisextractstheoriginatingexceptions,whichcanthenbehandledintheirowncatchclauses.

ThetechniqueofwrappingacheckedexceptioninaRuntimeExceptionwillbeusedwhenappropriatethroughouttherestofthisbook.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: