【Java学习笔记五】——异常处理

声明:本文章内容主要摘选自尚硅谷宋红康Java教程、《Java核心卷一》、《Java语言程序设计-基础篇》、廖雪峰Java教程,示例代码部分出自本人,更多详细内容推荐直接观看以上教程及书籍,若有错误之处请指出,欢迎交流。

一、处理错误

1.异常的类型

在理想状态下,用户输入数据的格式永远都是正确的,选择打开的文件也一定存在,并且永远不会出现bug。然而,在现实世界中却充满了不良的数据和带有问题的代码,现在是讨论Java程序设计语言处理这些问题的机制的时候了。

假设在一个Java程序运行期间出现了一个错误。这个错误可能是由于文件包含了错误信息,或者网络连接出现问题造成的,也有可能是因为使用无效的数组下标,或者试图使用一个没有被赋值的对象引用而造成的。用户期望在出现错误时,程序能够采用一些理智的行为。如果由于出现错误而使得某些操作没有完成,程序应该:返回到一种安全状态,并能够让用户执行一些其他的命令;或者允许用户保存所有操作的结果,并以妥善的方式终止程序。这个时候就轮到异常处理登场了,在讲到如何处理之前,我们可以认识一下异常的所有类型:

Exception
│
├─ RuntimeException
│  │
│  ├─ NullPointerException
│  │
│  ├─ IndexOutOfBoundsException
│  │
│  ├─ SecurityException
│  │
│  └─ IllegalArgumentException
│     │
│     └─ NumberFormatException
│
├─ IOException
│  │
│  ├─ UnsupportedCharsetException
│  │
│  ├─ FileNotFoundException
│  │
│  └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException

例如,如果使用一个越界的下标访问数组,程序就会产生一个ArrayIndexoutofBoundsException的运行时错误。为了从文件中读取数据,需要使用new Scanner(new File(filename))创建一个Scanner对象。如果该文件不存在,程序将会出现一个FileNotFoundException的运行时错误。
这些异常我们并不需要一一记住,在IDEA等软件上都可以帮助我们抛出异常,但在调试过程中出现异常提示时,如果我们知道该异常的具体意思,那么我们就可以迅速找出错误,所以建议记住常见的异常类型。

2.声明与抛出异常

如果遇到了无法处理的情况,那么Java的方法可以抛出一个异常。这个道理很简单:一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。例如,一段读取文件的代码知道有可能读取的文件不存在,或者内容为空,因此,试图处理文件信息的代码就需要通知编译器可能会抛出IOException类的异常。
方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类受查异常。例如,下面是标准类库中提供的FilelnputStream类的一个构造器的声明。

如:public FileInputStream(String name)throws FileNotFoundException

这个声明表示这个构造器将根据给定的String参数产生一个FileInputStream对象,但也有可能抛出一个FileNotFoundException异常。如果发生了这种糟糕情况,构造器将不会初始化一个新的FileInputStream对象,而是抛出一个FileNotFoundException类对象。如果这个方法真的抛出了这样一个异常对象,运行时系统就会开始搜索异常处理器,以便知道如何处理FileNotFoundException对象。
在自己编写方法时,不必将所有可能抛出的异常都进行声明。至于什么时候需要在方法中用throws子句声明异常,什么异常必须使用throws子句声明,需要记住在遇到下面4种情况时应该抛出异常:

  • 1)调用一个抛出受查异常的方法,例如,FilelnputStream构造器。
  • 2)程序运行过程中发现错误,并且利用throw语句抛出一个受查异常。
  • 3)程序出现错误,例如,a[-1]=0会抛出一个ArraylndexOutOfBoundsException这样的非受查异常。
  • 4)Java虚拟机和运行时库出现的内部错误。

RuntimeException、Error以及它们的子类都称为免检异常(unchecked exception)。所有其他异常都称为必检异常(checked exception),意思是指编译器会强制程序员检查并处理它们。
在大多数情况下,免检异常都会反映出程序设计上不可恢复的逻辑错误。例如,如果通过一个引用变量访问一个对象之前并未将一个对象赋值给它,就会抛出NullPointerException异常;如果访问一个数组的越界元素,就会抛出IndexoutofBoundsException异常。这些都是程序中必须纠正的逻辑错误。免检异常可能在程序的任何一个地方出现。为避免过多地使用try-catch块,Java语言不允许编写代码捕获或声明免检异常。

检测一个错误的程序可以创建一个正确异常类型的实例并抛出它。这就称为抛出一个异常(throwing an exception)。这里有一个例子,假如程序发现传递给方法的参数与方法的合约不符(例如,方法中的参数必须是非负的,但是传入的是一个负参数),这个程序就可以创建IllegalArgumentException的一个实例并抛出它,如下所示:

IllegalArgumentException ex = new IllegalArgumentException("Wrong Argument");
throw ex;

或者,如果你愿意,也可以使用下面的语句:
throw new IllegalArgumentException("Wrong Argument");

注意IllegalArgumentException是JavaAPI中的一个异常类。通常,JavaAPI中的每个异常类至少有两个构造方法:一个无参构造方法和一个带可描述这个异常的String参数的构造方法。该参数称为异常消息(exception message),它可以用getMessage()获取。

注意:声明异常的关键字是throws,抛出异常的关键字是throw。

3.创建异常类*

*表示了解即可,创建自定义异常类在初学时一般不需要使用,在实际开发中才会用到

在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。在这种情况下,创建自己的异常类就是一件顺理成章的事情了。我们需要做的只是定义一个派生于Exception的类,或者派生于Exception子类的类。例如,定义一个派生于IOException的类。
习惯上,定义的类应该包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器(超类Throwable的toString方法将会打印出这些详细信息,这在调试中非常有用)。

/*
一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。
BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生:
*/
public class BaseException extends RuntimeException {
}
//其他业务类型的异常就可以从BaseException派生:
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
...
//自定义的BaseException应该提供多个构造方法:
public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }
    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }
    public BaseException(String message) {
        super(message);
    }
    public BaseException(Throwable cause) {
        super(cause);
    }
}
//上述构造方法实际上都是原样照抄RuntimeException。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。

二、捕获异常

1.try-catch

如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。对于图形界面程序(applet和应用程序),在捕获异常之后,也会打印出堆栈的信息,但程序将返回到用户界面的处理循环中。

//要想捕获一个异常,必须设置try/catch语句块。如下所示:
try{
   ……
}catch(Exception1 exVar1){
   ……
}
catch(Exception2 exVar2){
   ……
}
catch(Exception3 exVar3){
   ……
}
//注意在catch块中异常被指定的顺序是非常重要的。如果父类的catch块出现在子类的catch块之前,就会导致编译错误。具体子父关系在文首的异常类型中可以查看

如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么程序将跳过try语句块的其余代码,执行catch子句中的处理器代码。
如果在try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会立刻退出。

//这就是一个简单的例子,涉及IO流的内容,在接下来的笔记将会讲到
public void read(String fileName) throws IOException {
        try {
            InputStream ips = new FileInputStream(fileName);
            int len;
            byte[] data = new byte[10];
            while((len = ips.read(data)) != -1){
                String str = new String(data, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2.finally

引言:世界上最遥远的距离,是我在if里你在else里,似乎一直相伴又永远分离;世界上最痴心的等待,是我当case你是switch,或许永远都选不上自己;世界上最真情的相依,是你在try我在catch。无论你发神马脾气,我都默默承受,静静处理。到那时,再来期待我们的finally。

有时候,不论异常是否出现或者是否被捕获,都希望执行某些代码。Java有一个finally子句,可以用来达到这个目的。finally子句的语法如下所示:

try{
   ……
}catch(Exception1 exVar1){
   ……
}
finally{
   ……
}

在任何情况下,finally块中的代码都会执行,不论try块中是否出现异常或者是否被捕获。考虑下面三种可能出现的情况:
1)如果try块中没有出现异常,执行finalstatements,然后执行try语句的下一条语句。
2)如果try块中有一条语句会引起异常,并被catch块捕获,然后跳过try块的其他语句,执行catch块和finally子句。执行try语句之后的下一条语句。
3)如果try块中有一条语句引起异常,但是没有被任何catch块捕获,就会跳过try块中的其他语句,执行finally子句,并且将异常传递给这个方法的调用者。
即使在到达finally块之前有一个return语句,finally块还是会执行。

public void read(String fileName) throws IOException {
        InputStream ips = null;
        //注意我们在try-catch-finally外声明ips,因为按照原来的做法,有可能try语句内的声明ips语句未能执行,但此时我们执行finally语句时便会出错
        try {
            ips = new FileInputStream(fileName);
            int len;
            byte[] data = new byte[10];
            while((len = ips.read(data)) != -1){
                String str = new String(data, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            ips.close();
        }
    }
      //实际上,最后的finally语句改为以下形式更好,不过上面为了便于理解
      finally {
            if (ips != null) {
                try {
                    ips.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

警告:当finally子句包含return语句时,将会出现一种意想不到的结果。假设利用returm语句从try语句块中退出。在方法返回前,finally子句的内容将被执行。如果finally子句中也有一个return语句,这个返回值将会覆盖原始的返回值。请看一个复杂的例子:

public static int f(int n){
        try{
            int r = n * n;
            return r;
        }finally{
            if(n == 2) return 0;
        }
    }
/*
如果调用f(2),那么try语句块的计算结果为r=4,并执行return语句。然而,在方法真正返回前,还要执行finally子句。
finally子句将使得方法返回0,这个返回值覆盖了原始的返回值4。
*/

Tips:选择需要增加try/catch/finally保护的代码,注意要完整的一行,在Eclipse中,可以通过快捷键Alt+ Shift + Z 快速调用try/catch/finally等结构,在IDEA中,快捷键则是Ctrl + Alt + T(实际上这些快捷键的是用来为选中的代码块快速添加结构,叫做surround with)

此笔记仅针对有一定编程基础的同学,且本人只记录比较重要的知识点,若想要入门Java可以先行观看相关教程或书籍后再阅读此笔记。

最后附一下相关链接:
Java在线API中文手册
Java platform se8下载
尚硅谷Java教学视频
《Java核心卷一》

原文地址:https://www.cnblogs.com/66ccffly/p/13459046.html