知识点梳理:Java异常

998 阅读8分钟

Java异常

前言

最近在好好整理自己学过的一些知识,打算把他们都好好地记录下来,完善个人的知识体系,坚持!冲冲冲

异常的分类

Java将异常分为Checked异常和Runtime异常两类。所谓Checked异常就是可以在编译器被处理的异常,Java要求必须显示的处理这些异常,或者抛出;是RunTimeException类以及它的子类的实例都被称为Runtime异常(反之就是Checked异常),这些异常无需强制显示的声明抛出

Checked异常体现了Java语言的严谨,它要求程序员要么显示的抛出,要么显示的进行捕获,但是。


异常的继承体系

所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分支:Error 和 Exception:

  • Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。 应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误, 除了通告给用户,并尽力使程序安全地终止之外, 再也无能为力了。这种情况很少出现。
  • 在设计 Java 程序时, 需要关注 Exception 层次结构。 这个层次结构又分解为两个分支:一个分支派生于 RuntimeException ; 另一个分支包含其他异常。划分两个分支的规则是:由程序错误导致的异常属于 RuntimeException ; 而程序本身没有问题, 但由于像 I/O 错误这类问题导致的异常属于其他异常,也就是上面👆说的Checked异常,这部分Java要求我们显示的去处理它们。

异常处理机制

  • try...catch

try块中的程序执行是出现异常,会生成一个异常对象并提交给Java运行时环境(这就是抛出异常),Java运行时环境收到异常对象会寻找可以处理该异常的catch块,如果有合适的,则将异常实例传递给catch块,若无则程序终止并结束。

当try块中的代码出现异常,将会停止当前执行(无法继续执行),直接转而执行对应异常catch块中的代码

try {
    // 业务代码
} catch (Exception e) {
    // 捕获异常
}

需要注意的是,无论代码是否处于try块中,出现异常都会生成一个异常实例

多个try...catch可以进行嵌套操作

  • 多异常捕获 (Java7引入)

自Java7开始,一个catch块可以捕获多个异常,捕获多异常时只需要在异常之间用 | 进行分割

try {
    // 业务代码
} catch (IndexOutOfBoundsException | NumberFormatException e) {
    // 捕获异常
}

需要注意的是,多异常捕获时,异常实例由final关键字修饰。无法对其进行重新赋值

  • 异常信息

异常对象包含了几个常用的方法,可以帮助我们查看异常信息

  1. getMessage() 返回异常描述的详细信息
  2. printStackTrace() 打印异常跟踪栈信息
  3. printStackTrace(PrintStream s) 将异常跟踪栈信息输出到输出流
  4. getStackTrace() 获取异常跟踪栈
  • try...catch...finally

除了try块、catch块,java还提供了finally块,无论try块中代码是否出现异常或者是catch块代码被执行,甚至是try块与catch块中执行了return语句,只要有finally块,其中的代码都会被执行

** 结合此特性,finally块一般被用来回收资源,比如数据库连接,网络连接等资源的回收 **

try {
    // 业务代码
}catch (Exception e) {
    // TODO: handle exception
} finally {
}

需要注意得是,异常处理语句中只有try块是必须的,catch块和finally都是可选,但至少出现其一,catch块必须位于try块之后,finally必须位于结尾

除非try块或者catch块中出现了退出虚拟机的方法 System.exit(1),否则无论try块或者catch块执行什么(这里重点说的就是return),都要执行完finally, 比如try或者catch中最后执行return,程序的执行也必须是先执行完finally中的代码,再返回去执行return。

  • 增强的try(自动关闭资源,Java7引入)

上面👆说了finally通常用来主动关闭一些资源,这样会显得程序比较臃肿,Java7提供了一种新的解决方案,可以自动关闭资源的try

try (InputStream ip = new FileInputStream("a.txt")) {
    // 业务
} catch (Exception e) {
    // TODO: handle exception
}

使用方法很简单,直接在try后面跟上(),并在其中定义好需要被自动关闭的资源,其中的资源会在try...catch的结束自动被关闭,这就是相当于包含了一个隐式的finaaly。

需要值得被关注的一点,只有实现了Closeable或者AutoCloseable接口的类才能被自动关闭,Java7中已经把几乎所有的资源类都实现了这两个接口

  • 异常抛出

上面👆介绍了如何捕获异常,但是对于异常的处理还有一种思路就是将其抛出。一般抛出的异常的思路就是此方法不知道该如何正确处理此异常,就将其抛出,转由其方法的调用者处理此异常。Java的main方法也可以进行异常抛出,此处抛出的异常将直接交给JVM进行处理,其处理的方式就是打印异常跟踪栈的信息并中止程序运行

Java提供throws关键字帮助我们抛出异常

public static void main(String[] args) throws Exception {
    // TODO Auto-generated method stub
    // 出现异常,由于throws,此异常将被抛出
}

使用throws需要注意,子类重写父类的方法,其抛出的异常的类型需要小于父类,且其数量也要少于父类(两小原则)

  • 主动抛出异常

一般的异常产生是由于代码等原因出现了某些问题,初次外,Java也允许程序主动抛出异常,需要使用throw关键字(注意此处区别与thorws关键字,throws用于方法级的错误抛出给上级, throw用于主动产生并抛出异常)

public static void main(String[] args) throws Exception {
    // TODO Auto-generated method stub
    // 出现异常,由于throws,此异常将被抛出
    throw new Exception("出现了一个异常");
}

一般的,我们使用throw来配合程序层级间异常的区分。在程序开发中,我们一般对于程序有一个严格的分层,底层的异常通常不愿意直接的暴露给用户,所以我们一般会在捕获到底层异常时进行相应的处理,并抛出一个更加适合的普通异常,再由上级程序进行捕获处理,这个过程也就是所谓的异常链。

举例说明,在企业级开发中,常常会有这样的分层结构,表现层(负责和用户交互),中间层(负责数据的处理),数据层(负责数据的存储)。比如出现了如SQLException这样的异常时我们通常时不愿意直接让表现层的代码捕获处理的,所以我们此时就需要对其进行一个转换,catch的同时通过使用throw换成一个业务异常,这样大大完善了程序调用的过程,保证了健壮性与合理性(这个过程称为异常转移,整个链路我们称为异常链)

Java7在throw的基础上做了一个小小的增强,从前的Java对于异常抛出的做法简单粗暴,捕获到的异常为Exception类型时,则只能声明throws Exception,但Java7后,当捕获到的异常虽然为Exception,但是编译器会自己去检测throw抛出的实际类型,从而我们在方法的签名中也可以更加细粒度的对于异常进行声明抛出


异常跟踪

class SelfException extends RuntimeException {

	public SelfException() {
		// TODO Auto-generated constructor stub
	}
	
	public SelfException(String msg) {
		// TODO Auto-generated constructor stub
		super(msg);
	}
	
}


public class PrintStackTraceTest {
	
	// 异常跟踪栈

	public static void main(String[] args) {
		firstMethod();
	}

	private static void firstMethod() {
		secondMethod();
	}

	private static void secondMethod() {
		thirdMethod();
	}

	private static void thirdMethod() {
		throwMethod();
	}

	private static void throwMethod() {
		throw new SelfException("自定义异常信息");
	}

	
}

执行上述的代码,得到如下结果

1处显示了此异常的详细描述信息 2处则自上向下完整的显示了异常跟踪栈,很明确的写出了所有异常的发生点

需要注意的是 打印跟踪栈这个方法虽然可以很好的追踪异常,但是我们在最后发布程序的时候应该避免使用它,转而应该捕获相应的异常并进行处理,而不是简单的对其进行打印


关于异常处理的建议

主要有四点:

  1. 不要过度的去使用异常,不能直接的将异常去替换流程控制语句,异常处理的初衷是为了将某些不可预期的错误与我们正常的代码作区分,所以绝对不要使用异常处理去代替任何流程控制;也不能因为有了throw就不再主动的编写任何的异常处理语句,直接简单的抛出异常。
  2. 不要写一个炒鸡大的try块,鸡块越大问题出现的可能越多,尽量将他们拆分为多个try...catch去分别捕获处理
  3. No Catch All,catch all就是我们捕获的时候直接定义Throwable or Exception,这样我们就不能区分各类异常,从而失去了对于不同异常的不同针对方法,同时这样的定义很可能会让我们漏掉某些关键的异常,因为所有的异常一视同仁了,这些异常被这样忽略一定不是好事
  4. 不要忽略那些异常,在捕获异常的处理代码中不要仅仅只是对于异常进行一个打印,我们需要对于他们采取适当的措施,合适的抛出,合适的处理,这样才称得上是好的代码