[软构/SC]checked exception VS unchecked exception

碎碎念:想写这个内容源于上课时突然意识到自己之前写实验的时候一直在使用IllegalArgumentException和NullPointerException作为输入错误时抛出的异常,怪不得我的编译器从来没有强制让我处理异常或抛出异常(还以为被编译器溺爱了)。听课的时候想到自己的代码尴尬得无地自容,只想赶紧下课改代码。
所以是很早以前就想写的内容,只是一直拖延到了快考试才有空写。。

在上面这段心路历程里,有一个很重要的事情就是,抛出这个异常时,编译器并没有要求代码进行处理。
什么是处理,怎么算处理,为什么应该处理,为什么有时候不需要处理?
接下来就来回答这四个问题。


1.异常是什么


        异常(Exception),顾名思义,是程序运行中出现的错误,是相对“正常”运行的概念。在Java中,当程序出现“意料之外”的问题时,会抛出(throw)异常提示程序员或用户,并且终止程序的运行。
        可以说,异常是一种程序无法正常执行的提示。通常,为了让用户有更好的体验,我们并不希望一出现什么错误程序就会崩溃,就好像用户轻轻碰了一下墙上凸出来的螺丝,整栋房子就垮掉了一样。
        在其他的章节中,我们学习了很多关于程序的正确性的知识,比如不满足前置条件时,程序直接结束。而了解到“异常”的概念后,我们希望程序不直接退出,而是能在推出前给出提示,根据这种提示维护程序的健壮性,在保持正确性的基础上,能最大程度地让程序保持运行状态,因此引入了异常抛出和异常处理。
题外话:健壮性又称鲁棒性,是程序抵御非预期状态的能力。如果对正确性和健壮性进行比较,个人认为正确性明显更加重要且首要。如果即使出现了错误结果,程序也不进行任何提示或修正,而是存储错误结果,并且有可能未来会被调用,那么这种潜在的危害性明显比即时中断的危害性更高,因为可能导致的危害和危害发生的时机都不能被外界准确掌握。


2.异常分什么类型


        上文提到,异常是“意料之外”的问题,是程序无法正常执行的提示。
        这种意料之外有的是“真”意料之外:程序员和用户都没有预料到会出现的问题,比如操作时间错乱,系统分配的存放数据的堆栈内存不足,试图访问数组上不存在的位置等,这类问题一般是电脑或者程序员自己导致的。这种异常中的一部分也称为错误(error),它们统称为“unchecked exception”。
        而有的是“假”意料之外:导致程序出现问题的原因是程序员和用户可以预料到的,但是这个因素还是被引入了,比如打开文件时找不到对应的文件导致打开失败,判断是否超重时用户输入了负数的身高或体重导致计算错误,将活动的起始日期设置在今天之前导致根本无法启动活动,系统设置了某种权限。这种异常出现的原因通常是明确的,程序员有办法也应该预料到可能出现的问题,并对它们进行处理,被认为是“checked excepetion”。


        因此,从“检查”和导致原因的角度,我们可以把广义的异常分为unchecked exception和checked exception
        checked exception是“经过检查”的异常,在Java中,一旦可能出现这种异常,编译器就会要求程序员采用某种方式进行处理,否则无法通过编译,属于“静态检查”的部分。
unchecked exception是“未经检查”的异常,即使某个方法可能抛出这种异常,编译器也不会对程序员有任何要求,只会在出现时打断程序的运行。
        进一步解释可能更难理解的unchecked exception。按照前面的解释可以发现,这些错误本来就不应该出现,编译器自然不会拿着锤头在后面追着你,让你逐个考虑这些错误的可能性。就好像住在一栋验收通过的房子里,房子不表现出任何可能倒塌的可能性,里面的人不太可能无时无刻考虑着房子倒塌该怎么办,这种行为虽然可行,但多少有杞人忧天之嫌。而另一个这种异常的特点就是,一旦出现,就会导致程序的崩溃,因为它们的出现意味着出现了某些不可逆转的外部因素,或者导致了一些无法挽回的后果,因此需要及时停止程序,检查导致崩溃的原因。

Error vs Exception

        下面这张图来自课程的ppt,我们可以发现,Java中,广义的异常(Throwable)从产生原因上看被分为checked和unchecked,而从类型上看被分为了Error和Exception
        前面我们讲到的“能否处理”是指从处理的必要性上来区分的,而error和exception则是从“是否允许处理”来区分的。在Java中,提供了对exception的处理方法(后面我们会讲到),而没有提供对error的处理方法,因为它们都是无法解决的,比如栈溢出、时序错误,或是程序逻辑有误。即使程序捕获到了他们,也有心无力。
        因此,当我们讨论异常处理时,不会讨论Error的处理,而是讨论Exception的处理。

课程ppt——可抛出类型的分类

        观察这张图,会发现在unchecked exception中并不全是error,还有一类“runtime exception”。这种异常同样是不需要处理的,但是出现的原因一般就不是外界的因素了,而是程序员自己没有对数据进行足够的检查,才会导致这些问题,如图中的“空指针错误”、“数组越界错误”,都是可以谨慎地处理以避免的(比如检查是不是空指针再访问其内容)。
        需要注意的是,不要像开头提到的那样,被名字所迷惑,认为“空指针”、“非法参数”等与checked exception中的IOException是同样的含义。虽然一些情况下,确实是由于用户输入导致出现参数不合法,但是无论是含义还是程序对待它们的方式都是截然不同的
        不过在我个人看来,虽然不符合规矩,但是并不是不能适时利用这些runtime exception,比如向一个构造好的类传递参数,将检查交给对方,可能可以简化一部分代码


3.如何使用异常——怎么抛出,怎么算处理

        讨论完异常的含义,接下来聚焦到使用上。

        前面我们只提到了处理的问题,但在异常需要被处理前,需要经历“出现”的过程,也就是“抛出(throw)”。抛出一个异常很简单,就是在需要的地方写一个throws new XXException /XXError。初学常用的Exception是IOException,标志用户输入错误,而会使用的error是AssertionError,这在后文详述。
        在抛出后,可能需要考虑处理和声明的问题。
        对于unchecked exception,抛出后就不用管了,在抛出的方法不需要处理,调用这个方法的地方也可以不进行处理。
        而对checked exception,在抛出后一定要对进行处理。对它的处理有两种可能性。
        第一种处理方法是使用throws向外传递,即,在抛出异常的地方不对异常进行处理,而是交给外界调用这个方法的程序去处理。这种传递可以是无限,只要不想处理,就可以向外抛出可能的异常。最坏的情况是异常传递到了主程序(main),而主程序也没有处理,直接抛给了用户并终止了程序,这种做法是合法的,但是会使得checked exception的存在毫无意义。
       
通常与throws配套的是写在方法specification(spec)里的@throws标签,用于向其他人说明程序可能会抛出什么异常和抛出异常的原因。
        注意,向外传递和声明使用的都是关键字throws,而抛出异常使用的是throw。
        第二种处理方法是使用try-catch(-finally)一套招式进行处理,无论是代码内部抛出的异常,还是调用某个方法传递的异常,都可以用这个方法终结它。
        try后面是可能出现异常的代码段;catch后面是捕获到某种类型的异常时进行什么样的操作,在参数列表里通常会比较详细地写出某种异常的类型,而不是直接使用父类Exception,这可能导致不正确地处理意料之外的异常,就像医生用同一把手术刀做不同类型的手术一样;finally是可选的代码段,表示无论有没有异常出现,都一定会执行的操作,比如尝试读取文件内容后,无论是否成功,都要关闭文件。
        在方法异常抛出后,除非在方法内被处理,否则该方法后面的程序都不会执行

        下面这个例子演示了异常的传递和处理。首先进入的是main方法,在其中调用B方法,使用try-catch处理可能出现的异常;进入B方法后,调用A方法;A方法中发现a小于0,于是抛出异常,不进行a的打印,然后B中接收到了A传递来的异常,也直接退出方法;在main方法中捕捉到了这个异常,打印文字。执行结果是打印“调用B时出现异常”。整个过程与栈调用相同,层层调用,异常层层返回。

public static void main(String[] args) {
    try{
        B(-1);
    }catch(IOException e){
        System.out.println("调用B时出现异常");
    }
}
public void B(int a) throws IOException{
    A(a);//抛出异常
    System.out.println("我在B里");//不执行
}
public void A(int a) throws IOException
    if(a<=0) throw new IOException;
    System.out.println(a);//不执行
}

         下面这个例子演示分支和finally的使用。①如果a大于0,则打印“a大于0”,然后打印a的值,打印end;②如果a小于等于0,则打印“a小于等于0”,然后打印a的值和end。finally的程序段必然会执行,而由于异常被处理,try-catch-finally段后面的程序也得以执行。

public void A(int a)
    try{
        if(a<=0) throw new IOException;
        System.out.println("a大于0");
    }catch(IOException e){
        System.out.println("a小于等于0");
    }finally{
        System.out.println(a);
    }
    System.out.println("end");
}
    

//2024.05.22增加一点补充

        在向外传递异常时,可以传递多种异常,在throws后面用逗号隔开,但是异常及其子类型的异常不建议同时写在里面。
        而处理异常时,可以用 “ | ” 分隔不同类型的异常,分隔的异常不能存在继承关系(如IOException和FileNotFoundException),但是可以使用多个catch依次抓取子类型异常和父类型异常。使用方式如:

catch(FileNotFoundException|EOFException  e){...}

try{...
}catch(FileNotFoundException e){...
}catch(IOException e){...}//上下两个catch的顺序不能颠倒

AssertionError

        断言(assert)是Java中的一种除错机制,目的是检查程序执行过程中是否满足某个必要的约束条件,以此检查程序的正确性。而这个方法等价于throw new AssertionError (), 实质上就是抛出一个异常(错误)。
        AssertionError是error中比较特殊的一个,其他error的出现多半是因为机器、系统故障等出现故障,而AssertionError一般就是因为程序的逻辑出现了问题。通常根据代码必须满足的约束条件进行编写,放在专门的方法中对程序运行的正确性进行检查。
        回想学习使用assert的场景,是在ADT的学习中,我们被要求为rep invariant编写checkRep方法,所谓“表示不变性”,就要求数据和抽象映射之间不能出错,一旦出错,就必然出现程序逻辑问题,需要检查代码的逻辑。


        结合使用场景和error的含义,不难认识到,AssertionError不是给调用方法的人看的,而是给写方法的人看的, 如果写方法的人不修改,无论重启程序多少次,都一定会在这个分支口出现错误。所以说,AssertionError是一个特殊的,由程序员自己抛出,再由自己接收的异常,出现这个异常就是提示我们自己:“嘿,你代码有问题,再练练吧”。
        在正确的程序中,这个错误是一定不能出现的,更不能出现在客户端和用户面前。因此,断言通常只用于开发和调试阶段,在生产环境(正式发布)中通常会屏蔽;相应的,checkRep等确保程序满足约束条件的方法也应该被屏蔽,以提升程序性能(checkRep可能会有大量遍历、搜索)。

后记

        对exception深有感触,是因为在实验中切实地误解了这个内容;而对AssertionError的认识是在一次程序调试中,发现assert内的内容没有被执行,才依照编译器的提示进行了修改,在那以后就常用该方法作为程序错误的提示;当认识到checkRep是“对自己的约束”这一点时,也有种修仙突破瓶颈的感觉。

        在实验中,我真切地感受到了实践促进认识发展的真理;而自己想明白概念、规范性方法的含义时,也感觉到无比地畅快。十年二十年后,我必然遗忘了实验的内容,那些写过的ADT也没有任何意义,但是当我回想起某个晚上,我写实验时的灵光一现,或许依然会为那时的自己而感动。勤思考,多发问,真正地理解学了什么,这或许才是完整完成一个实验的意义。

  • 50
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值