一.什么是异常

  异常就是发生在程序的执行期间,破坏程序指令的正常流程的事件。当方法中出现错误时,该方法会创建一个对象并将其交给运行时系统。该对象称为异常对象,它包含有关错误的信息,包括错误的类型和出现错误时程序的状态。创建异常对象并将其交给运行时系统的行为称为抛出异常。

  在方法抛出异常后,运行时系统会尝试在调用栈中查找可以处理它的程序。调用栈是指从最开始的方法到出现错误的方法以及之间的所有方法列表,下图是一个调用栈:



  运行时系统在调用栈中查找包含可以处理异常的代码块的方法。这个代码块称为异常处理器。搜索从发生错误的方法开始,并按照调用方法的相反顺序继续查找。找到适当的处理程序后,运行时系统会将异常传递给处理程序。如果抛出的异常对象的类型与处理程序可以处理的类型匹配,就认为异常处理程序是合适的。

  如果运行时系统穷举搜索调用栈上的所有方法而没有找到适当的异常处理程序,如下图所示,则运行时系统(以及程序)就会终止,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。

二.异常的分类

  在Java中,异常对象都是派生于Throwable类的一个实例。如果Java中内置的异常类不能满足需求,用户可以创建自己的异常类。

  下面是Java异常层次结构的一个示意图:



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

  Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不该抛出这种类型的对象。如果出现了这样的内部错误,除了通知用户,并尽力使程序安全地终止之外,便再也无能为力了。这种情况很少出现,也不需要我们关心。

  在设计Java程序时,需要关注Exception层次结构。这个层次结构又分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。划分两个分支的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。

  常见的RuntimeException有NullPointerException、ClassCastException、NumberFormatException、IndexOutOfBoundException等,这些异常都是由于编写代码时考虑地不全面而导致的。“如果出现RuntimeException异常,那么就一定是你的问题”是一条相当有道理的规则。例如,应该通过检测数组下标是否越界来避免数组下标越界异常,应该通过在使用变量之前检测是否为null来杜绝空指针异常的发生。

  还有一些异常,并不是由于代码的问题。例如,当我们删除文件时,有可能这个文件并不存在,这时候就会抛出一个异常。与RuntimeException不同,此时我们并不需要修改原有的代码,但是我们可以在抛出异常后执行一些其他的措施,例如提示用户检查输入的文件路径。

  Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受检异常,所有其他的异常称为受检异常。受检异常必须要进行处理,而非受检异常既可以处理,也可以不处理。

三.捕获并处理异常

  本节将介绍如何使用try、catch和finally块来处理异常。此外,在Java SE 7中,还引入了带资源的try语句,它适用于那些使用可关闭资源的情景。

1.try块

  构建一个异常处理器的第一步就是使用try块包围可能出现异常的代码。看下面的例子:

public void createFile() {
File file = new File("D:\\bar.txt");
try {
if(file.createNewFile()) {
System.out.println("Create file successfully!");
}
}
catch and finally blocks ...
}

  上面的createFile方法的作用是在D盘根目录下创建一个bar.txt文件。在调用File类的createNewFile方法时,可能会出现一个IOException。而这个异常是一个受检异常,必须对它进行处理,因此我们使用一个try块将可能出现异常的语句包围。如果try块中发生异常,则该异常由与其关联的异常处理程序处理。

2.catch块

  通过在try块后使用catch块来提供异常处理程序。如果try块中的代码可能出现多种异常,可以使用多个catch块来分别对应不同的异常:

try {
// ...
} catch (ExceptionType name) {
// ...
} catch (ExceptionType name) {
// ...
}

  如果在try语句块中的任何代码抛出了一个在catch块中说明的异常类,那么

  1. 程序将跳过try块的其余代码;
  2. 程序将执行catch块中的代码;
  3. 程序继续执行catch块之后的代码。

  如果在try块中的代码没有拋出任何异常,那么程序将跳过catch块。

  如果try块的代码拋出了一个与任何catch块声明的异常类型都不匹配的异常,那么这个方法就会立刻退出。

  下面我们给上面的例子加上catch块:

public void createFile() {
File file = new File("D:\\bar.txt");
try {
if(file.createNewFile()) {
System.out.println("Create file successfully!");
}
} catch {
System.out.println("Program threw an exception,please check the path of file.");
}
}

  可以在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。按照下列方式为每个异常类型使用一个单独的catch子句:



  在Java SE 7中,同一个catch子句中可以捕获多个异常类型。例如,假设对应缺少文件和未知主机异常的动作是一样的,就可以合并catch子句:

3.finally块

  考虑这样一个问题:我们需要与数据库建立连接并执行一些操作,当操作数据库时可能会出现异常,因此我们需要使用try块包围可能出现问题的代码,并在catch块中对这些问题进行处理。无论在操作数据库时是否出现了问题,我们都需要在最后关闭与数据库的连接来释放资源。但关闭数据库连接的操作既不能放在try块中,因为程序可能在此之前就出现异常而进入catch块;也不能放在catch块中,因为程序可能并没有出现异常。此时可以将关闭数据库连接的操作放在finally块中。不管是否有异常被捕获,finally子句中的代码都会被执行。因此,finally块经常被用来关闭资源。在下一小节中,我们将会看到一个更加优雅的关闭资源的方式。

  上面的例子可以表示为:

try {
// connect to database
// operate database
} catch {
// handle exception
} finally {
// close connection
}

  try语句可以只有finally子句,而没有catch子句。例如:

try {
// connect to database
// operate database
} finally {
// close connection
}

  在上面的try-finally结构中,无论在try语句块中是否遇到异常,finally块中的语句都会被执行。如果try块中抛出一个异常,异常会在finally块中的语句执行完之后将异常重新抛出。但是,如果finally块中也出现异常,那么try块中抛出的异常将会被丢弃,程序将会抛出finally块中的异常。

finally块中的return语句

  我们知道,finally块中的语句一定会执行,那么是否try块或catch块中的return语句会被finally块中的return语句(如果有的话)覆盖呢?下面来看一个例子:

public static String returnInFinally() {
try {
return "try";
} finally {
return "finally";
}
}

  调用这个方法,返回值是"finally"而不是"try"。也就是说,finally块中的return语句会覆盖try或catch块中的return语句。当try块或catch块中的代码执行至return语句时,程序会进入finally块中继续执行,最后执行finally块中的return语句。

  finally块中的return语句不仅会覆盖try块和catch块内的返回值,还会丢弃try块或catch块中的异常,就像异常没有发生一样。例如:

public static int returnInFinally() {
try {
int i = 2 / 0;
return i;
} finally {
return 1;
}
}

  上面的代码中,2/0将会触发ArithmeticException,但是由于finally块中有return语句,因此这个异常将会被丢弃。

  一般来说,应该尽量避免在finally块中使用return语句或抛出异常,除非确实有必要这么做。

4.带资源的try语句

  带资源的try语句是指声明了一个或多个资源的try语句。资源是在程序结束时必须关闭的对象。带资源的try语句可以保证在try块结束时关闭资源。任何实现了AutoCloseable接口的对象都可以被看作是资源。

  下面的方法从文件中读取第一行,它使用了一个BufferedReader来从文件中读取数据,BufferedReader是程序完成后必须关闭的资源:

static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}

  在此示例中,try语句中声明的资源是一个BufferedReader对象。声明语句出现在try关键字后面的括号内。BufferedReader类实现了接口AutoCloseable。因为BufferedReader实例是在try语句中声明的,所以无论try语句是正常完成还是出现异常(readLine方法可能会抛出IOException),它都将被关闭。可以这样理解,编译器会自动为我们生成一个finally块并调用资源的close方法来关闭资源。

  还可以指定多个资源,例如:

try(Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF-8");
PrintWriter out = new PrintWriter("out.txt")) {
while(in.hasNext()) {
out.println(in.next().toUpperCase());
}
}

  当从try块中(无论是否出现异常)退出时,在try语句中所声明的资源的close方法都会被自动调用,并且是按与资源声明相反的顺序来调用的。

  不同于try-catch-finally和try-finally,带资源的try语句中,如果try块和关闭资源时同时出现异常,程序将会抛出try块中的异常,而关闭资源时出现的异常将会被抑制,可以通过异常对象的getSuppressed()方法获取被抑制的异常(注意,这里是“抑制”而不是“丢弃”,被丢弃的异常无法通过getSuppressed()获取)。

  带资源的try语句也可以有catch块和finally块,不过它们会在资源关闭后才会执行。

5.一个完整的例子

  在上面的几小节我们分别学习了try、catch和finally。现在我们来编写一个同时包含这三部分的例子:

public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entering try statement");
out = new PrintWriter(new FileWriter("OutFile.txt");
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}

  这个方法的执行顺序无非两种:要么try块中的代码出现异常,程序进入对应的catch块并执行,最后执行finally块中的代码;要么try块中的代码正常结束,然后执行finally块中的代码。

四.声明方法抛出的异常

  前面我们讨论了当程序中出现受检异常时应该如何处理。然而,有时候我们不想处理或者需要调用当前方法的方法去处理,此时我们可以不编写异常处理程序,但是需要将可能出现的异常声明在方法名称后面。声明异常的语法如下:

modifiers returnType methodName(parameter list) throws Exception1, Exception2, ...

  例如,上面的writeList方法可以改写为:

public void writeList() throw IOException {
PrintWriter out = null;
try {
System.out.println("Entering try statement");
out = new PrintWriter(new FileWriter("OutFile.txt");
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}

  在上面的程序中,如果try块中的代码抛出异常,由于没有对应的异常处理程序,异常将会继续传递到调用writeList的方法中,在这个方法中也有两种处理方式:要么处理异常,要么继续传递异常。

  由于无需为非受检异常编写异常处理程序,因此也就无需将非受检异常声明在方法名称之后。

五.抛出异常

  有时候,我们的程序可能本身并没有出现任何异常,但是程序已经进入了错误的逻辑,并且我们需要将这些信息告诉调用当前方法的程序,此时可以手动抛出一个异常。例如,我们编写一个计算平方根的方法,这个方法接受非负整数作为参数。如果其他程序在调用这个方法时传递了一个负数,那么返回任何值都是错误的,此时就需要我们抛出一个异常,告诉该程序当前的错误信息。

  使用throw关键字来手动抛出一个异常。如下所示:

public double squareRoot(int x) throws Exception{
if(x < 0) {
throw new Exception("Wrong argument");
}
return Math.sqrt(x);
}

  当在其他方法中调用这个方法时,必须处理这个可能出现的异常。

六.异常链

  有时我们对异常的操作可能是将其捕获后再抛出新的异常,例如下面的例子:

public class ExceptionChainDemo {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
System.out.println("Second stackTrace:");
e.printStackTrace();
}
} private static void method1() throws Exception {
try {
method2();
} catch (Exception e) {
System.out.println("First stackTrace:");
e.printStackTrace();
throw new Exception("Exception from method1");
}
} private static void method2() throws Exception {
throw new Exception("Exception from method2");
}
}

  该程序的输出如下:



  可以看到,method2抛出异常后,在method1中将其捕获并打印调用栈;然后重新抛出另外一个异常,并在main方法中将其捕获并打印调用栈。在第二次打印调用栈时,之前的异常信息已经丢弃,我们只能看到method1中抛出了一个异常。

  可以通过链式异常来保存之前的异常信息。也就是说,可以通过之前的异常来构造新的异常。下面是有关的几个方法:

Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)

  getCause方法可以获取被当前异常包装的异常。而initCause方法和另外两个构造方法可以将需要保存的异常包装进当前异常。例如,如果我们要保存method2中抛出的异常,可以像下面这样修改method1:

private static void method1() throws Exception {
try {
method2();
} catch (Exception e) {
System.out.println("First stackTrace:");
e.printStackTrace();
throw new Exception("Exception from method1", e);
}
}

  再次运行程序,输入如下:



  可以看到,第二次抛出的异常中包含了第一次抛出的异常的信息。

七.自定义异常类

  在程序中,可能会遇到任何标准异常类都没有能够充分描述清楚的问题。在这种情况下,我们就可以自己创建一个异常类。

  如果需要创建非受检异常,可以创建一个RuntimeException类的子类。当然,实际上很少需要创建非受检异常,更多情况下我们创建的都是受检异常。在创建非受检异常时,需要继承Exception类或其他非受检异常类。

  下面自定义了一个受检异常,它继承了IOException:

public class FileFormatException extends IOException {
public FileFormatException() {}
public FileFormatException(String message) {
super(message);
}
}

  现在我们就可以在代码中使用自己定义的异常了。

Java基础教程(22)--异常的更多相关文章

  1. Java基础教程:JDBC编程

    Java基础教程:JDBC编程 1.什么是JDBC JDBC 指 Java 数据库连接,是一种标准Java应用编程接口( JAVA API),用来连接 Java 编程语言和广泛的数据库. JDBC A ...

  2. Java基础教程(18)--继承

    一.继承的概念   继承是面向对象中一个非常重要的概念,使用继承可以从逻辑和层次上更好地组织代码,大大提高代码的复用性.在Java中,继承可以使得子类具有父类的属性和方法或者重新定义.追加属性和方法. ...

  3. Java基础教程(12)--深入理解类

    一.方法的返回值   当我们在程序中调用方法时,虚拟机将会跳转到对应的方法中去执行.当以下几种情况发生时,虚拟机将会回到调用方法的语句并继续向下执行: 执行完方法中所有的语句: 遇到return语句: ...

  4. Java基础教程:网络编程

    Java基础教程:网络编程 基础 Socket与ServerSocket Socket又称"套接字",网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个s ...

  5. Java基础教程:反射基础

    Java基础教程:反射基础 引入反射 反射是什么 能够动态分析类能力的程序称为反射. 反射是一种很强大且复杂的机制. Class类 在程序运行期间,Java运行时系统始终为所有对象维护一个被称为运行时 ...

  6. Java基础教程:面向对象编程[3]

    Java基础教程:面向对象编程[3] 内容大纲 基础编程 获取用户输入 java.util.Scanner 是 Java5 的新特征,我们可以通过 Scanner 类来获取用户的输入.我们可以查看Ja ...

  7. Java基础教程:Java内存区域

    Java基础教程:Java内存区域 运行时数据区域 Java虚拟机在执行Java程序的过程种会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟 ...

  8. Java基础教程:IDEA单元测试

    Java基础教程:IDEA单元测试 环境配置 使用idea IDE 进行单元测试,首先需要安装JUnit 插件. 安装JUnit插件步骤 File-->settings-->Plguins ...

  9. [SQL基础教程] 2-2 算数运算符和比较运算符

    [SQL基础教程] 2-2 算数运算符和比较运算符 算数运算符 四则运算 运算符 含义 + - * / SELECT col_1*2 AS col_new FROM table; 注意 所有包含NUL ...

随机推荐

  1. laravel5的Bcrypt加密方式对系统保存密码的小结

    laravel5文档介绍 //对 A 密码使用Bcrypt 加密 $password = Hash::make('secret'); //你也可直接使用 bcrypt 的 function $pass ...

  2. 虚幻4随笔6 Object和序列化

    诚如之前所说,虚幻4主要的一些特性都是由UObject穿针引线在一起的,想把虚幻玩到比较深的程度,UObject是迟早要面对.回避不得的问题,所以,准备在其它主题之前,先把UObject好好弄一下.U ...

  3. Java的动态编译、动态加载、字节码操作

    想起来之前做的一个项目:那时候是把需要的源代码通过文件流输出到一个.java文件里,然后调用sun的Comipler接口动态编译成.class文件,然后再用专门写的一个class loader加载这个 ...

  4. JIT与JVM的三种执行模式:解释模式、编译模式、混合模式

    Java JIT(just in time)即时编译器是sun公司采用了hotspot虚拟机取代其开发的classic vm之后引入的一项技术,目的在于提高java程序的性能,改变人们“java比C/ ...

  5. Java 日志学习

    Java 日志学习,主要学习log4j,(为了查找方便,直接拷贝别人文章:原文:http://www.cnblogs.com/xt0810/p/3659045.html) [结构] java日志对调试 ...

  6. 配置阿里yum源,设置命令

    配置阿里yum源 #linux的软件包管理 安装 软件的方式有三种 .源代码编译安装() .下载python3的源代码 .解压缩源代码 .进入源代码目录,开始编译安装 .配置环境变量 .yum方式安装 ...

  7. leetcode 75. 颜色分类 JAVA

    题目: 给定一个包含红色.白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色.白色.蓝色顺序排列. 此题中,我们使用整数 0. 1 和 2 分别表示红色.白色和 ...

  8. docker 网络实践

    #docker 网络模式 环境 centos7. , Docker version -ce docker自带网络类型 bridge,host,none,container,overlay,macvla ...

  9. docker 运行容器,安装Nginx

    ########################################## #运行容器 #安装Nginx #搜索.下载镜像 docker search nginx docker pull n ...

  10. thinkphp5的mkdir() Permission denied问题

    最近一直在用tp5写项目,在此遇到的问题也比较多.今天来谈谈“mkdir() Permission denied”错误. 你如果不仅仅写代码,还得部署到线上,那么这个tp5的这个错误,你有很大概率会遇 ...