任何一个程序都有可能发生异常,本节将主要讲解关于 Java 的异常处理。主要内容包含异常的基础理论以及如何捕获异常。

知识点

第一节 异常处理概述
第二节 Java异常处理类

  • 2.1 Throwable

    • 2.1.1 Throwable有五种构造方法
    • 2.1.2 Throwable的所有成员方法
  • 2.2 Error

  • 2.3 Exception

  • 2.4 RuntimeException

  • 2.5 Checked Exception

  • 2.6 Uncheck Exception

  • 2.7 总结

第三节 Java异常处理执行流程探究

  • 3.1 流程一
  • 3.2 流程二
  • 3.3 流程三
  • 3.4 流程四
  • 3.5 流程五
  • 3.6 流程六
  • 3.7 流程七
  • 3.8 流程八
  • 3.9 总结

第四节 Java异常处理实践原则

  • 4.1 使用异常,而不使用返回码
  • 4.2 利用运行时异常设定方法使用规则
  • 4.3 消除运行时异常
  • 4.4 正确处理检查异常
  • 4.5 使主流程代码保持整洁
  • 4.6 使用try-with-resources
  • 4.7 尽量处理最具体的异常
  • 4.8 设计自己的异常类型要遵循的原则



异常概述
异常指不期而至的各种状况,它在程序运行的过程中发生。作为开发者,我们都希望自己写的代码永远都不会出现 bug,然而现实告诉我们并没有这样的情景。如果用户在程序的使用过程中因为一些原因造成他的数据丢失,这个用户就可能不会再使用该程序了。所以,对于程序的错误以及外部环境能够对用户造成的影响,我们应当及时报告并且以适当的方式来处理这个错误。
之所以要处理异常,也是为了增强程序的鲁棒性
异常都是从 Throwable 类派生出来的,而 Throwable 类是直接从 Object 类继承而来。你可以在 Java SE 官方 API 文档中获取更多关于它们的知识。



异常分类
异常通常有四类:

  • Error:系统内部错误,这类错误由系统进行处理,程序本身无需捕获处理。
  • Exception:可以处理的异常。
  • RuntimeException:可以捕获,也可以不捕获的异常。
  • 继承 Exception 的其他类:必须捕获,通常在 API 文档中会说明这些方法抛出哪些异常。

平时主要关注的异常是 Exception 下的异常,而 Exception 异常下又主要分为两大类异常,一个是派生于 RuntimeExcption 的异常,一个是除了 RuntimeExcption 体系之外的其他异常。
RuntimeExcption 异常(运行时异常)通常有以下几种:

  • 错误的类型转换
  • 数组访问越界
  • 访问 null 指针
  • 算术异常

一般来说,RuntimeException 都是代码逻辑出现问题。
非 RuntimeException(受检异常,Checked Exception)一般有:

  • 打开一个不存在的文件
  • 没有找到具有指定名称的类
  • 操作文件异常

受检异常是编译器要求必须处理的异常,必须使用 try catch 处理,或者使用 throw 抛出,交给上层调用者处理。


声明及抛出

throw 抛出异常

当程序运行时数据出现错误或者我们不希望发生的情况出现的话,可以通过抛出异常来处理。
异常抛出语法:

image.png

image.png

image.png

通过这个方法,我们可以看出,在java中,Throw是抛出的意思, throw new NullPointerException()
新建了一个异常类,并调用无指针异常。

在运行ThrowTest的时候,执行了 new NullPointerException方法,这个方法的功能就是,定位异常发生的地点(一般是某个方法中),并且指明是发生了什么问题。

throws 声明异常

throws 用于声明异常,表示该方法可能会抛出的异常。如果声明的异常中包括 checked 异常(受检异常),那么调用者必须捕获处理该异常或者使用 throws 继续向上抛出。throws 位于方法体前,多个异常之间使用 , 分割。


image.png

捕获异常
通常抛出异常后,还需要将异常捕获。使用 try 和 catch 语句块来捕获异常,有时候还会用到 finally。
对于上述三个关键词所构成的语句块,try 语句块是必不可少的,catch 和 finally 语句块可以根据情况选择其一或者全选。你可以把可能发生错误或出现问题的语句放到 try 语句块中,将异常发生后要执行的语句放到 catch 语句块中,而 finally 语句块里面放置的语句,不管异常是否发生,它们都会被执行。
你可能想说,那我把所有有关的代码都放到 try 语句块中不就妥当了吗?可是你需要知道,捕获异常对于系统而言,其开销非常大,所以应尽量减少该语句块中放置的语句。

image.png
image.png

在异常控制中,try catch在我一开始的理解中,有点类似于if的判断语句
if(出现某个情况) 解决问题的语句。

那么,既然if就可以处理问题,为什么我们还需要异常处理呢?

这里的原因是,我们在使用if语句的时候,我们是知道,哪些语句会发生问题,哪个对象可能存在问题,如果我们一开始就知道那些地方有问题,我们为什么不直接在编写程序的时候,就解决这些问题呢?

异常处理相比于我们的if条件判断,关键点在于,我们不知道什么地方会出现问题,一开始是没有问题的,问题主要是出现在不同的方法直接的交互上,有可能其他方法的传参有问题,又或者是其他对象的调用是有问题的,if主要处理的是方法自己的内部矛盾,异常处理的是方法和方法之间协同的矛盾。

当我们使用try catch语句的时候,我们其实是在“监听”这些语句,如果出现异常,就捕获。

在java中,我们用异常类来新建异常对象,异常对象拥有着多种处理异常的方法,比如说在此例中

ClassNotFoundException e
e.printStackTrace();

printStackTrace();方法会追踪哪里出了问题,并且输出给用户。

相比于if语句,异常的功能多得多,但同时异常处理对计算机的开销也是很大的。


捕获多个异常
在一段代码中,可能会由于各种原因抛出多种不同的异常,而对于不同的异常,我们希望用不同的方式来处理它们,而不是笼统的使用同一个方式处理,在这种情况下,可以使用异常匹配,当匹配到对应的异常后,后面的异常将不再进行匹配。

image.png

image.png

在处理异常时,并不要求抛出的异常同 catch 所声明的异常完全匹配,子类的对象也可以匹配父类的处理程序。比如异常 A 继承于异常 B,那么在处理多个异常时,一定要将异常 A 放在异常 B 之前捕获,如果将异常 B 放在异常 A 之前,那么将永远匹配到异常 B,异常 A 将永远不可能执行,并且编译器将会报错。


自定义异常
尽管 Java SE 的 API 已经为我们提供了数十种异常类,然而在实际的开发过程中,你仍然可能遇到未知的异常情况。此时,你就需要对异常类进行自定义。
自定义一个异常类非常简单,只需要让它继承 Exception 或其子类就行。在自定义异常类的时候,建议同时提供无参构造方法和带字符串参数的构造方法,后者可以为你在调试时提供更加详细的信息。
百闻不如一见,下面我们尝试自定义一个算术异常类。

image.png

image.png

image.png

在ExceptionTest中,当循环执行到( i == 0 )时,throw new MyAriException(“This is an exception occured.”);

这个时候,方法遇到了问题,但并没有编写解决问题的方法,所以方法把该问题(异常)抛出

throw new MyAriException让MyAriException()方法来解决,MyAriException继承了ArithemeticException类,在ArtithemeticException类(系统自带)中,自带了很多处理异常的方法,之前在ExceptionTest中遇到的问题,将由ArtithemeticException来解决。

同时我们也发现,在ExceptionTest类中,我们调用的是MyAriException类的方法,后者的修饰词是Public,说明方法可以被共用。

异常堆栈
当异常抛出后,我们可以通过异常堆栈追踪程序的运行轨迹,以便我们更好的 DEBUG。

image.png

image.png

通过上面的异常堆栈轨迹,在对比我们方法的调用过程,可以得出异常信息中首先打印的是距离抛出异常最近的语句,接着是调用该方法的方法,一直到最开始被调用的方法。从下往上看,就可以得出程序运行的轨迹。

总结
本节主要内容是对 Java 的异常处理进行讲解,主要包含以下知识点:

  • 异常分类
  • 声明及抛出
  • 捕获异常
  • 自定义异常
  • 异常堆栈

请大家务必手动完成代码并运行对比结果,这样才能更好的理解并掌握 Java 关于异常处理。


JAVA 异常类型结构

Throwable 是所有异常类型的基类,Throwable 下一层分为两个分支,Error 和 Exception.

image.png

Error 和 Exeption

  • Error
    Error 描述了 JAVA 程序运行时系统的内部错误,通常比较严重,除了通知用户和尽力使应用程序安全地终止之外,无能为力,应用程序不应该尝试去捕获这种异常。通常为一些虚拟机异常,如 StackOverflowError 等。
  • Exception
    Exception 类型下面又分为两个分支,一个分支派生自 RuntimeException,这种异常通常为程序错误导致的异常;另一个分支为非派生自 RuntimeException 的异常,这种异常通常是程序本身没有问题,由于像 I/O 错误等问题导致的异常,每个异常类用逗号隔开。

受查异常和非受查异常

  • 受查异常
    受查异常会在编译时被检测。如果一个方法中的代码会抛出受查异常,则该方法必须包含异常处理,即 try-catch 代码块,或在方法签名中用 throws 关键字声明该方法可能会抛出的受查异常,否则编译无法通过。如果一个方法可能抛出多个受查异常类型,就必须在方法的签名处列出所有的异常类。
    通过 throws 关键字声明可能抛出的异常
    private static void readFile(String filePath) throws IOException {
    File file = new File(filePath);
    String result;
    BufferedReader reader = new BufferedReader(new FileReader(file));
    while((result = reader.readLine())!=null) {
    System.out.println(result);
    }
    reader.close();}

try-catch 处理异常
private static void readFile(String filePath) {
File file = new File(filePath);
String result;
BufferedReader reader;
try {
reader = new BufferedReader(new FileReader(file));
while((result = reader.readLine())!=null) {
System.out.println(result);
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
}}

  • 非受查异常
    非受查异常不会在编译时被检测。JAVA 中 Error 和 RuntimeException 类的子类属于非受查异常,除此之外继承自 Exception 的类型为受查异常。

异常的抛出与捕获

直接抛出异常

通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。

image.png

封装异常再抛出

有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。

image.png

捕获异常

在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理
image.png

同一个 catch 也可以捕获多种类型异常,用 | 隔开

image.png


第一节 异常处理概述

在理想境界中,程序永远不会出现问题,用户输入的数据永远是正确的,逻辑没有任何问题 ,选择打开的文件也一定是存在的,内存永远是够用的……反正没有任何问题!但是一旦出现这些问题,如果处理不好,程序就不能正常运行了,用户就有可能再也不使用这个程序了。
要处理异常,必先知道异常发生的原因;要知道异常发生的原因,必先知道异常发生的场景。你的程序可能和任何其他实体交互的时候,都可能发生异常。Java程序中,都是利用方法(Method)和其他实体进行交互。所以异常的发生、抛出、声明和处理都是在方法内。下图是Java程序可能和其他实体交互的概要图:
图1:你的方法与其他实体交互概要图

image.png
如图1所示,你写的方法和外部实体交互大概可以分为五类:

  1. 和资源(Resource)交互,见图⑤处。这里资源的范围很广,比如进程外部的数据库,文件,SOA服务,其他各种中间件;进程内的类,方法,线程……都算是资源。
  2. 给进程内的其他方法(User Method)提供服务,见图②处。
  3. 依赖进程内的其他方法(Server Method),见图③处。包括Java平台提供的方法和其他第三方供应方提供的方法。
  4. 和系统环境交互,见图⑧处。系统环境可能是直接环境——JVM,也可能是间接环境——操作系统或硬件等。
  5. 给外部实体提供服务,见图①处。这种外部实体一般会通过容器(或其他类似的机制)和你的方法进行交互。所以,可以归为②,不予探讨。

Java方法和每一类实体进行交互时,都可能发生异常。
当和资源交互时,常常会因为资源不可用而发生异常,比如发生找不到文件、数据库连接错误、找不到类、找不到方法……等等状况。有可能是直接产生的,见图⑤处;有可能是间接产生的,比如图⑥处发生异常,Server Method把异常抛给Your Method,图③处就间接发生了异常。一般来说,你写的方法间接发生这类异常的可能性比直接发生要大得多,因为直接产生这类异常的方法在Java平台中已经提供了。对于这类异常,通常有以下几个特点:

  • 问题来自外部,你的方法本身的正常流程逻辑没问题。
  • 这类异常通常是暂时的,过段时间就可用了(经过资源维护者的彻夜奋战……)。最终用户通常也可以接受暂时的等待或采取替补方案。
  • 你的程序的其他功能还可以用。

这时,你的方法应该这样处理:

  • 返回到一种安全状态,并能够让用户执行一些其他的命令(比如支付宝支付失败时返回一个弹出框,说明余额不足等原因,让用户重新选择其他支付渠道);或者
  • 允许用户保存所有操作的结果,并以适当的方式终止程序(比如保存填了一半的表单)。

然后,你应该协调各方,促进资源恢复可用,消除异常。
当给用户方法(User Method )提供服务时,用户可能会传入一些不合法的数据(或者其他不恰当的使用方法),进而对程序的正常流程造成破坏。你的方法应该检查每一个输入数据,如果发现不合法的数据,马上阻止执行流程,并通知用户方法。
当调用服务方法(Server Method )时,有可能会发生两类异常。一类是你的使用方法不正确,导致服务中止;一类是服务方法出了异常,然后传递给你的方法。如果是第一种异常,你应该检查并修改你的方法逻辑,消除BUG。对于第二类异常,你要么写一个处理器处理,要么继续传递给上层方法。
当和系统环境交互时,有可能因为JVM参数设置不当,有可能因为程序产生了大量不必要的对象,也有可能因为硬故障(操作系统或硬件出了问题),导致整个程序不可用。当这类异常发生时,最终用户没法选择其他替代方案,操作到一半的数据会全部丢失。你的方法对这类异常一般没什么办法,既不能通过修改主流程逻辑来消除,也不能通过增加异常处理器来处理。所以通常你的方法对这类异常不需要做任何处理。但是你必须检查进程内的所有程序和系统环境是否正常,然后协调各方,修改BUG或恢复环境。
Java的异常都是发生在方法内,所以研究Java异常,要以你设计的方法为中心。我们以“你的方法 ”为中心,总结一下处理办法:当服务方法告诉“你的方法 ”的主流程逻辑有问题时,就要及时修复BUG来消除异常;当用户方法非法使用“你的方法”时,应该直接中止主流程,并通知用户方法,强迫用户方法使用正确的方式,防止问题蔓延;当服务方法传递一个异常给“你的方法”时,你要判断“你的方法”是否合适处理这个异常,如果不合适,传递给上层方法,如果合适,写一个异常处理器处理这个异常。当系统环境出了问题,“你的方法”什么也做不了。
刚才以“你的方法”为中心,总结了在“你的方法”内部的处理办法。现在以“你”为中心,总结一下方法外部的处理方法:当资源不可用的时候,你应该协调各方,恢复资源;当发生系统故障时,你应该协调各方,恢复系统。
现在,已经基本分析清楚了异常发生的原因,以及相应的应对方法。下一节正式介绍Java异常处理机制。

第二节 Java异常处理类

Java把异常当做是破坏正常流程的一个事件,当事件发生后,就会触发处理机制。
Java有一套独立的异常处理机制,在遇到异常时,方法并不返回任何值(返回值属于正常流程),而是抛出一个封装了错误信息的对象。下图是Java异常处理机制类层级结构图:
图2:Java异常处理机制类层级结构图
image.png

2.1 Throwable

所有的异常对象都派生于Throwable类的一个实例。

2.1.1 Throwable有五种构造方法:

image.png
备注:

  • suppression:被压抑的异常。想了解更多信息,请参看我的译文“try-with-resources语句”。
  • strack trace:堆栈跟踪。是一个方法调用过程列表,它包含了程序执行过程中方法调用的具体位置。

**2.1.2 Throwable的所有成员方法:

image.png
image.png
image.png
备注:

  • 所有派生于Throwable类的异常类,基本都没有这些成员方法,也就是说所有的异常类都只是一个标记,记录发生了什么类型的异常(通过标记,编译期和JVM做不同的处理),所有实质性的行为Throwable都具备了。
  • 综上,在一个Throwable里面可以获取什么信息?
    • 获取堆栈跟踪信息(源代码中哪个类,哪个方法,第几行出现了问题……从当前代码到最底层的代码调用链都可以查出来)
    • 获取引发当前Throwable的Throwable。追踪获取底层的异常信息。
    • 获取被压抑了,没抛出来的其他Throwable。一次只能抛出一个异常,如果发生了多个异常,其他异常就不会被抛出,这时可以通过加入suppressed异常列表来解决(JDK7以后才有)。
    • 获取基本的详细描述信息

从图2可以看出,Throwable类只有两个直接继承者:Error和Exception。然后Exception又分为RuntimeException和Checked Exception。

2.2 Error

在Java中, 由系统环境问题引起的异常,一般都继承于Error类。
对于Error类:

  • 一般开发者不要自定义Error子类,因为它代表系统级别的错误。与一般的程序无关。
  • 在Java异常处理机制中,Error不强制捕获或声明,也就是不强制处理。因为程序本身对此类错误无能为力。一般情况下我们只要把堆栈跟踪信息记录下来就行。

下列是Java平台中直接继承于Error的错误类型:
AnnotationFormatError, AssertionError, AWTError, CoderMalfunctionError, FactoryConfigurationError,FactoryConfigurationError, IOError, LinkageError, ServiceConfigurationError, ThreadDeath,TransformerFactoryConfigurationError, VirtualMachineError

2.3 Exception

在Java中,除了系统环境问题引起的异常,一般都继承于Exception类。Exception分为RuntimeException和Checked Exception。Checked Exception必须要捕获或声明。而RuntimeException不强制。
对于Exception类:

  • 如果你创建了一个异常类型,直接继承于Exception,那么这个异常类型将属于检查异常(Checked Exception)。

2.4 RuntimeException

在Java中,由于接口方法使用不当造成的异常,一般属于RuntimeException,也就是运行时异常。
对于RuntimeException:

  • 如果你调用服务方法的方式不正确,你应该马上修改代码,避免发生RuntimeException
  • 如果是用户方法调用你的方法的方式不正确,你应该立刻抛出RuntimeException,强制让使用者修正代码或改变使用方式,防止问题蔓延
  • 一般情况下,不要捕获或声明RuntimeException。因为问题在于你的程序本身有问题,如果你用异常流程处理了,反而让正常流程问题一直存在

下列是Java平台中直接继承于RuntimeException的运行时异常:
AnnotationTypeMismatchException, ArithmeticException, ArrayStoreException, BufferOverflowException,BufferUnderflowException, CannotRedoException, CannotUndoException, ClassCastException,CMMException, ConcurrentModificationException,DataBindingException, DOMException,EmptyStackException, EnumConstantNotPresentException, EventException,FileSystemAlreadyExistsException, FileSystemNotFoundException, IllegalArgumentException,IllegalMonitorStateException,IllegalPathStateException, IllegalStateException, IllformedLocaleException,ImagingOpException,IncompleteAnnotationException, IndexOutOfBoundsException,JMRuntimeException, LSException, MalformedParameterizedTypeException,MirroredTypesException,MissingResourceException, NegativeArraySizeException,NoSuchElementException, NoSuchMechanismException, NullPointerException, ProfileDataException,ProviderException, ProviderNotFoundException, RasterFormatException, RejectedExecutionException,SecurityException, SystemException, TypeConstraintException, TypeNotPresentException,UndeclaredThrowableException, UnknownEntityException, UnmodifiableSetException,UnsupportedOperationException, WebServiceException, WrongMethodTypeException

2.5 Checked Exception

在Java中,直接或间接因为“资源”问题引起的异常,一般属于检查异常(Checked Exception) 。检查异常继承于Exception,而不继承于RuntimeException。
对于检查异常:

  • 必须捕获或声明
  • 交给关心这个异常的方法处理
  • 异常处理器应该引导用户接下来怎么办,至少做到安全退出

下列是Java平台中直接继承于Exception的检查异常:
AclNotFoundException, ActivationException, AlreadyBoundException, ApplicationException, AWTException,BackingStoreException, BadAttributeValueExpException, BadBinaryOpValueExpException,BadLocationException, BadStringOperationException,BrokenBarrierException, CertificateException,CloneNotSupportedException, DataFormatException, DatatypeConfigurationException, DestroyFailedException,ExecutionException, ExpandVetoException, FontFormatException, GeneralSecurityException,GSSException,IllegalClassFormatException, InterruptedException, IntrospectionException, InvalidApplicationException,InvalidMidiDataException, InvalidPreferencesFormatException, InvalidTargetObjectTypeException,IOException, JAXBException, JMException,KeySelectorException, LastOwnerException,LineUnavailableException, MarshalException, MidiUnavailableException, MimeTypeParseException,MimeTypeParseException, NamingException, NoninvertibleTransformException, NotBoundException,NotOwnerException, ParseException, ParserConfigurationException, PrinterException, PrintException,PrivilegedActionException, PropertyVetoException, ReflectiveOperationException, RefreshFailedException,RemarshalException, SAXException,ScriptException, ServerNotActiveException, SOAPException,SQLException, TimeoutException, TooManyListenersException, TransformerException, TransformException,UnmodifiableClassException, UnsupportedAudioFileException, UnsupportedCallbackException,UnsupportedFlavorException, UnsupportedLookAndFeelException, URIReferenceException, URISyntaxException, UserException, XAException, XMLParseException, XMLSignatureException,XMLStreamException, XPathException

2.6 Uncheck Exception

Error和RuntimeException统称为非检查异常。两者的共同点就是都不被强制捕获或声明。实际上两者描述问题的范围完全没有交集。

2.7 总结

所有的功能都在Throwable类里面实现了,子类只需要直接继承或间接继承它,并且加上需要的构造方法就行(一般而言,第一第二个构造方法是必须的,也可以全部加上),而且构造方法通常只需要一行代码:super(…),也就是说只要调用父类的构造方法就行了。Java把异常分为三类(Error,Checked Exception,RuntimeException),只是在语法层面上有不同的标记而已。它们自身拥有的功能一样,运行时系统处理它们的方式也是一样的(你也可以捕获或声明非检查异常),不同的是编译器对它们的区别对待(检查异常必须要在代码里处理,非检查异常就不需要),以及程序员对它们的区别对待(这需要程序员遵循良好的实践原则)。
这三类异常全部覆盖了第一节中所描述的异常发生场景,图1中,④⑤⑥处可能会发生Checked Exception,②③处既可能会发生RuntimeException也可能会发生Checked Exception,⑦⑧⑨处可能会发生Error。①处已经超出了Java异常处理机制的范畴(这属于容器要考虑的问题),通常在数据中加入返回码来通知异常信息。
理解了每一类异常对应的场景,很多人其实已经知道该怎么用了,不必往下看了。

第三节 Java异常处理执行流程探究

首先设计两个方法,一个方法可能会抛出RuntimeException,一个方法可能会抛出Checked Exception。

image.png

**3.1 流程一
image.png

执行结果:

image.png

分析:

违反了runtimeServerMethod方法的使用规则——入参不能为null,导致产生了一个运行时异常。主流程线程直接中断,后面的代码不再执行。

3.2 流程二

image.png

执行结果:

image.png


分析:
RuntimeException也可以捕获处理(这是一种不好的实践),运行时系统并不会区分异常类型。异常发生以后,try代码块后面的代码不再执行,而是跳到catch代码块,线程不中断,执行完整个方法。

3.3 流程三

image.png

执行结果:
image.png

分析:
当符合服务方法的规则时,就不会抛出运行时异常。方法就可以正常执行完成。

3.4 流程四

image.png

执行结果:

image.png

分析:

当调用了checkedServerMethod方法,并且发生了Checked Exception时,一定要捕获或声明该异常,否则编译不通过。上例中,异常发生后,try代码块后面的代码不再执行,跳到catch代码块,再执行finally代码块(在这里有关闭资源的操作),然后再执行其余部分。

3.5 流程五

image.png

执行结果:

image.png

3.9 总结

  • 在运行时环境,并不会区分异常的类型,所以程序员自己要遵从良好的实践原则,否则Java异常处理机制就会被误用。
  • 然后finally代码块总是会在方法返回或方法抛出异常前执行,而try-catch-finally代码块后面的代码就有可能不会再执行。
  • finally代码块里面不推荐使用return语句或throw语句。
  • try代码块一定要求要有一个catch代码块或finally代码块(二者取其一就行)。
  • catch处理器的优先级比声明异常语句要高。
  • 如果多处抛出异常,finally代码块里面的异常会压抑其他异常。

    第四节 Java异常处理实践原则

    4.1 使用异常,而不使用返回码

    关于这一点,在我的译文“使用异常的优势”有很详细的描述。理解了这一点,程序员们才会想要使用Java异常处理机制。

    4.2 利用运行时异常设定方法使用规则

    很常见的例子就是,某个方法的参数不能为空。在实践中,很多程序员的处理方式是,当传入的这个参数为空的时候,就返回一个特殊值(最常见的就是返回一个null,让用户方法决定怎么办)。还有的处理方式是,自己给一个默认值去兼容这种不合法参数,自己决定怎么办。这两种实践都是不好的。
    对于第一种处理方式,返回值是用来处理正常流程的,如果用来处理异常流程,就会让用户方法的正常流程变复杂。一次调用可能不明显,当有多个连续调用就会变得很复杂了。对于第二种处理方式,看起来很强大,因为“容错”能力看起来很强,有些程序员甚至可能会为此沾沾自喜。但是它也一样让正常流程变复杂了,这不是最糟糕的,最糟糕的是,你不知道下一次用户会出什么鬼点子,传个你现有处理代码处理不了的东西进来。这样你又得加代码,继续变复杂……BUG就是这样产生的。
    好的实践方式就是,设定方法的使用规则,遇到不合法的使用方式时,立刻抛出一个运行时异常。这样既不会让主流程代码变复杂,也不会制造不必要的BUG。为什么是运行时异常而不是检查异常呢?这是为了强迫用户修改代码或者改正使用方式——这属于用户的使用错误。

    4.3 消除运行时异常

    当你的程序发生运行时异常,通常都是因为你使用别人的方法的方式不正确(如果设计这个异常的人设计错误,就另当别论。比如设计者捕获一个检查异常,然后在处理器抛出一个运行时异常给用户。如果遇上这样的供应商,还是弃用吧)。所以,一般都是采取修改代码的方式,而不是新增一个异常流程。

    4.4 正确处理检查异常

    处理检查异常的时候,处理器一定要做到下面的要求才算合格:

  • 返回到一种安全状态,并能够让用户执行一些其他的命令;或者

  • 允许用户保存所有操作的结果,并以适当的方式终止程序。

不好的实践案例一:因为有的异常发生的概率很小,有些程序员就会写出下面的代码:

image.png

catch代码块里面什么都不写!或者只在里面打一个log。这样既不会传递到上层方法,又不会报编译错误,还不用动脑筋……

不好的实践案例二:捕获一个检查异常,什么都不做(或只打一个log),然后抛出一个运行时异常:

image.png

这样也不会让上层方法感觉到这个异常的存在,也不会报编译错误了,也不用动什么脑筋……

在案例一中,一旦出现了异常,try代码块里的代码没执行完,用户要求做的事情没做完,却又没有任何反馈或者得到一个错误反馈。
在案例二中,一旦出现了异常,try代码块里的代码没执行完,虽然把运行时异常抛给用户了,用户也不会去处理这个异常,又没有办法通过改变使用方式消除异常,直接让用户代码崩溃掉。
对于检查异常,好的实践方式是:

  • 让可以处理这个异常的方法去处理。衡量的标准就是在你这个方法写一个处理器,这个处理器能不能做到本节开头的那两个要求,如果不能,就往上抛。如果你不能知道所有用户的所有需求,你通常就做不到那两个要求。
  • 有必要的时候可以通过链式异常包装一下,再抛出。
  • 最终的处理器一定要做到本节开头的那两个要求。

    4.5 使主流程代码保持整洁

    一个try代码块后面可以跟多个catch代码块,这就让一些可能会发生不同异常的代码可以写在一块,让代码看起来很清晰。相反,在一个方法里写多个try-catch,或者写嵌套的try-catch,就会让主流程代码变得很混乱。

    4.6 使用try-with-resources

    请参看我的译文“try-with-resources语句”。
    try-with-resources语句比起普通的try语句,干净整洁的多。而且最终抛出的异常是正常流程中抛出的异常。

    4.7 尽量处理最具体的异常

    尽量使用最具体的异常类作为处理器匹配的类型。这样处理器就不用兼顾很多种情形,不易出错。从Java7开始,一个处理器可以处理多种异常类型。
    注意:同一个try语句中,比较具体的异常的catch代码块应写在前面,比较通用的异常的catch代码块应写在后面。

    4.8 设计自己的异常类型要遵循的原则

    当你是一个模块开发者,你就很有必要设计一组或多组自己的异常类型。一般情况下,要遵守如下原则:

  • 确定什么场景下,需要创建自己的异常类型:(参看我的译文“创建异常类”)。

  • 为你的接口方法的使用规则创建一组运行时异常。
  • 包装别人的检查异常的时候,一定也要用检查异常。这样异常才能传递给上层方法处理。
  • 设计一组有层次结构的异常,而不是设计一堆零零散散的异常。
  • 区分清楚异常发生的原因,然后决定你的异常是检查异常还是运行时异常。
  • 模块内部不需要处理自己定义的异常。

Java异常处理机制的目的至少有三个:一是归类处理不同的异常,二是提供足够的信息方便调试,三是让主流程代码保持整洁。