JavaSE系列之异常处理

    在平时的代码编写和程序设计过程中,我们会遇到各种问题,比如用户输入错误、设备错误、代码错误等。现阶段(即学习JavaSE阶段)最常见的是代码错误,比如数组下标越界,文件不存在等,这些问题需要我们开发人员通过编码来处理。

    当然,JAVA程序设计语言拥有异常(Exception)处理机制,开发人员需要利用该机制来处理以上遇到的各种问题。

    首先,我们来了解一下官方对异常的定义:当程序违反Java编程语言的语义约束时,Java虚拟机会将此错误作为异常发送给程序。一个简单的例子就是数组下标越界访问。通俗点来说,导致程序无法正常运行的叫异常。

一、异常分类

    在Java中,所有的异常对象都是Throwable类或其子类的一个实例。下图是Java异常类型的层次结构图。

图1 Java异常类结构图
图1. Java异常类结构图

    如图中所示,Throwable类及其所有子类均为异常类。其中,Error类和Exception类是Throwable类的直接子类,Error类是通常程序不会从中恢复的所有异常的父类,这类异常描述了Java运行时系统的内部错误和资源耗尽错误,程序员通常对其无能为力;相反Exception类是程序希望可以从中恢复的所有异常的父类,这部分应该是程序员所关注的,其子类RuntimeException是由程序代码错误导致的异常,所以程序员更应该去关注这部分异常。

    可将异常分为三类:错误(Error)、运行时异常(RuntimeException)、受查异常(checked exception)。

    其中,JAVA语言规范将Error类和RuntimeException类及其它们子类的所有异常称为非受查异常(unchecked exception),除非受查异常之外的所有其他异常称为受查异常(checked exception),即Throwable类、Exception类和其除RuntimeException类及其子类外的所有子类。

    我们可直接使用Java中内置的异常类,也可根据需要自定义异常类型,通常我们将新异常类定义为受查异常,即作为Exception类的子类声明,而不是RuntimeException类的子类。

二、发生异常的原因

    通常来说,导致异常发生的原因有三:

  1. 执行了throw语句抛出异常。
  2. Java虚拟机检测到异常执行条件,即:违反Java语言正常语义的表达式计算,如整数除以零;加载、链接或初始化程序的一部分时发生错误,此时会抛出LinkageError类子类的实例;内部错误或资源限制阻止Java虚拟机实现Java语言的正常语义,此时抛出VirtualMachineError类子类的实例。此类异常不会在程序的任意点抛出,而是为某处表达式求值或语句执行的可能结果。
  3. 发生异步异常(asynchronous exception)。

    经查询官方资料得知,通常大多数异常由于线程的执行而同步发生,并且在程序中被指定为导致该异常的确切点。异步异常是可能在程序执行的任何时刻发生的异常。异步异常只发生在以下情况:

  • 调用Thread或ThreadGroup类的stop方法。
  • Java虚拟机中的内部错误或资源限制,阻止它实现Java编程语言的语义。在这种情况下,抛出的异步异常是VirtualMachineError的子类的实例。

三、声明异常

    我们经常可以看到声明方法时,在后面声明该方法可能抛出的异常,如:

public File openFile(String path) throws FileNotFoundException{ }

该方法根据传入的path参数打开相应文件并返回,而该文件有可能不存在,所以需要声明该方法可能抛出FileNotFoundException异常。如果文件存在,该方法可以正常打开文件并返回相应的File对象,如果文件不存在,该方法将不能正常返回File对象,而是抛出一个FileNotFoundException类对象。

   我们只需要声明抛出受查异常即可,对于Error类及其子类的错误异常,我们无能为力,因为其是不可控制的;而对于RuntimeException类及其子类的异常,我们要避免发生,因为其是完全可控的,我们只需要注意程序代码的正确性就可以避免这类异常。

    我们自己平时在编写方法时,要注意出现以下情况时需要抛出异常:

  1. 调用一个抛出受查异常的方法,如上面openFile()方法。
  2. 程序运行过程中发现错误,并利用throw语句抛出一个受查异常(后面会提到throw语句抛出异常)。
  3. 程序出现错误,如数组下标访问越界。
  4. Java虚拟机和运行时库出现的系统错误。

如果出现第一种或第二种情况,我们必须声明有可能抛出的异常来告诉该方法的调用方,以便调用方编写处理器来捕获该异常。

四、异常处理

    平时编写代码的过程中,我们对于所遇到的异常一般有两种处理方式:第一种是如上面第三部分声明异常中所提在方法后面声明抛出可能发生的异常,此种方法可一直传递下去给调用方,但最终还是需要某个调用方利用第二种方法对其进行处理,所以此处着重介绍一下第二种方法,即利用try{ }catch(){ }...语句对异常进行捕获,基本语法如下:

try{
    产生异常的代码;
} catch(异常类型 e) {
    处理异常的代码;
} finally {
    最后一定执行的代码;
}

将有可能产生异常的代码放在try代码块中,将对应处理异常的代码放在catch代码块中。若try代码块中的某条语句产生了异常,则try代码块中后续的语句将不会执行,转而执行catch代码块中的语句,最后再执行finally代码块中的语句;若try代码块中的语句正常执行,并未产生任何异常,则会跳过catch代码块中的语句直接执行finally代码块中的语句。catch代码块中多用语句e.printStackTrace()来输出堆栈轨迹来查看异常信息。

    我们如何确定一个异常是该被捕获还是被抛出呢?如果我们知道如何处理产生的异常,则应该捕获这些异常并对其进行相应的处理;相反,如果我们不知道如何处理,则应该抛出这些异常将其传递下去以便调用方对其进行进一步的处理。

【注】在编写子类方法覆盖父类方法的时候,只能声明抛出更特定的异常(子类方法抛出的异常是父类方法抛出异常的子类)或者不抛出异常。如果父类方法没有抛出任何受查异常,子类方法就不能抛出任何异常,即子类方法需要捕获代码中可能出现的每个异常。

    如果代码有可能产生多个异常,则可以通过捕获多个异常来解决,语法如下:

try{
    产生多个异常的代码;
} catch(异常类型1 e) {
    处理异常类型1的代码;
} catch(异常类型2 e) {
    处理异常类型2的代码;
} catch(异常类型3 e) {
    处理异常类型3的代码;
} finally {
    最后一定执行的代码;
}

    在JDK7中具有一个新特性,可在同一个catch语句中捕获多个异常,语法如下:

try{
    产生多个异常的代码;
} catch(异常类型1 | 异常类型2 e) {
    处理异常类型1或2的代码;
} catch(异常类型3 e) {
    处理异常类型3的代码;
} finally {
    最后一定执行的代码;
}

此时异常类型1和异常类型2之间不存在父类和子类关系才可正确使用。

【注】当try{ }catch(){ }finally{ }中出现return语句时,我们来看看如下方法的返回值会是什么?

public static int testTryReturn() {
	try {
	    return 1;
	} catch(Exception e) {
		return 2;
	} finally {
		return 3;
	}
}

正常来看,在执行try代码块时,语句return 1会结束整个方法的运行,将控制权交给调用方,故而该方法的返回值为1,可事实是什么样子呢,请看下面的截图:

我们可以看到最终方法的返回值为3,说明finally代码块执行了。虽然try代码块中有return语句,但是在方法返回前,finally代码块中的语句将被执行,所以3会覆盖掉原来的返回值1,最终方法的返回值为3。所以这意味着finally代码块中的return语句会覆盖掉try代码块和catch代码块中的return语句,即使try代码块和catch代码块产生了异常,该方法也因为finally代码块中return语句的存在而无法正常返回异常信息,这也正是图片中出现黄色警告的原因,我们要避免这种情况。

猜你喜欢

转载自blog.csdn.net/Mr_Programming_Liu/article/details/87715326
今日推荐