Core Java 笔记(八)- 异常

异常

 

程序运行期间不可避免会出现错误,可归为用户输入错误、设备错误、物理限制、代码错误等情况。当出现错误而使得某些操作无法完成时,程序应该具备以下能力:

  • 返回到一种安全状态,让用户能执行其他命令

  • 允许用户保存操作结果,妥善终止程序

异常处理(exception handing)的任务就是在错误发生时将控制权转移到合适的地方。Java 提供了异常处理机制,当某个方法不能正常完成它的任务时,可以通过另外一个路径退出方法,抛出(throw)一个封装了错误信息的对象,异常处理机制会搜索能够处理该异常状况的异常处理器(exception handler)。

 

一、异常分类

 

下图是 Java 异常层次结构的简化:

所有的异常都是从 Throwable 继承而来,在下一层分解为两个分支:Error 和 Exception,其中 Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误,这种情况很少出现,不是我们必须关注的,一旦出现就只能尽量让程序安全终止。我们需要关注的是 Exception 层次结构,它又分解为两个分支:一个派生于 RuntimeException,另一个包含其他异常。

 

RuntimeException

如何界定?

  • RuntimeException(运行时异常):由程序错误导致的异常

  • 其他异常:程序本身没有问题,像 I/O 错误这类问题导致的异常

派生于 RuntimeException 的异常包括但不限于下面几种情况:

  • ClassCastException / 类型转换错误 

  • ArrayIndexOutOfBoundsException / 数组访问越界

  • NullPointerException / 访问 null 引用

其他异常则比如:

  • IOException / 试图在文件尾部之后读取数据

  • FileNotFoundException / 试图打开不存在的文件

  • ClassNotFoundException / 试图根据给定的(不正确)字符串查找 Class 对象

 

unchecked / checked 

对于运行时异常,应该由编写程序的人解决,因为它只取决于代码本身。而其他异常还会受到不可预估的环境影响,比如一个文件可能在我检查它是否存在之前就已经被删除了。故此,把属于 Error 和 RuntimeException 的异常称为非受查异常(unchecked),其他异常则称为受查异常(checked),所谓受查,指的是编译器会检查是否为它们提供了对应的异常处理器

 

二、抛出受查异常

 

throws

方法应该在其首部用 throws 关键字声明所有可能抛出的受查异常,作用在于,告知这个方法的调用者,并交由其处理,如果这个方法的调用者不对它进行处理,将无法通过编译。而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。

声明异常的方式如下:

public FileInputStream(String name) throws FileNotFoundException {...}

// 可能抛出多个
public Image loadImage(String name) throws FileNotFoundException, EOFException {...}

如果在子类中覆盖了超类的一个方法,那么子类方法中声明的异常不能比超类所声明的更加通用,也就是说,要么更具体,要么不声明。如果超类方法没有声明会抛出任何异常,那么子类在覆盖它时也不能声明。

注意,throws 只是说明了发生某种异常的可能,只有在方法执行的过程中确实发生异常时,才会把相应的异常对象传递给这个方法的调用者。

 

throw

对于已经存在的异常类,要将其抛出,只需要三步:

  1. 找到一个合适的异常类

  2. 创建它的一个对象

  3. 抛出(throw)

下面来看看抛出一个异常的具体例子,有一个名为 readData 的方法正在读取一个文件,这个文件在首部指出了其内容的长度为 1000 个字符,然而在读到 700 个字符后,文件就结束了。这可能会导致后面的程序出现不正确的结果,所以我希望在这里抛出一个异常:

throw new EOFException();
// or:
// EOFException e = new EOFException();
// throw e;

EOFException 还有一个接收 String 参数的构造器,可以更具体地描述异常情况,把这一部分组合到原先的代码中:

String readData(Scanner in) throws EOFException {
    // ...
    while (...) {
        if (!in.hasNext()) {
            String detailOfTheException = "Content-length: " + len + ", Received: " + n;
            if (n < len)  throw new EOFException(detailOfTheException);
        }
        // ...
    }
    return ...
}

 

自定义异常类

有时候我们会遇到某些问题,无法使用标准的异常类恰当地描述,于是就可以定义自己的异常类(派生于 Exception 或其子类):

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

 

三、捕获异常

 

如果有某个异常发生了,但没有进行捕获,那么程序就会终止执行,并在控制台打印出异常信息(异常的类型和堆栈内容)。要想捕获一个异常,必须使用 try / catch 语句块。

 

try / catch

简单形式:

try {
    // your code here
} catch (ExceptionType e) {
    // handler for this type
}

如果 try 语句块的某处代码抛出了一个异常,并且符合 catch 子句中指出的类型,那么程序将:

  1. 跳过 try 语句块中的其余代码

  2. 执行 catch 子句中的处理器代码

具体例子:

public void readData(String filename) {
    try {
        InputStream in = new FileInputStream(filename);
        int nextByte;
        while (nextByte = in.read() != -1) {
            ...
        }
    } catch (IOException exception) {
        exception.printStackTrace();
    }
}

综合以上,如果调用了一个抛出受查异常的方法,那么就只能有两个选择:要么继续传递(throws),要么就地捕获并处理(try / catch)。哪种选择更好呢?一般来说,对那些知道如何处理的异常应该捕获和处理,而对不知道(可能了解的信息还不足)如何处理的异常应该继续传递。当然了,这是一个灵活的选择,重要的是对当前编写的方法功能有清晰的定位。

 

捕获多个异常

一个 try 语句块中可能会出现多个异常类型,假如想对不同类型的异常做出不同处理,可以为每个异常类型配对一个单独的 catch 子句:

try {
    // your code here
} catch (FileNotFoundException e) {
    // for missing files
} catch (UnknownHostException e) {
    // for unknowm hosts
} catch (IOException e) {
    // for other I/O problems
}

如果对某几类异常的处理是相同的,可以放在一起,合并 catch 子句,并且生成的字节码只包含一个对应公共 catch 子句的代码块(会更高效),写法如下:

try {
    // your code here
} catch (FileNotFoundException | UnknownHostException e) {
    // ...
} catch (IOException e) {
    // ...
}

注意,只有当捕获的异常类型彼此不存在继承关系时才需要这么做。

 

再抛出

可以在 catch 子句中再次 throw 一个异常,目的在于改变异常的类型,提高灵活性,比如:

try {
    // access the database
} catch (SQLException e) {
    throw new ServletException("database error: " + e.getMessage());
}

还可以使用包装技术,先创建一个新的异常对象,再将原始异常设置为新异常的原因:

try {
    // ...
} catch (SQLException e) {
    Throwable se = new ServletException("data error");
    se.initCause(e);
}

这样,当捕获到异常时,就可以通过下面的语句重新得到之前的异常:

Throwable previousException = se.getCause();

有时候可能只想记录一个异常,而不想做任何改变,重新抛出:

try {
    // ...
} catch (Exception e) {
    logger.log(level, message, e);
    throw e;
}

 

四、处理资源

 

在 try 子句中发生异常后,剩余的代码就不会执行,这就给资源回收带来了问题,比如在读取一个本地文件时出现 IOException ,方法终止,但此时输出流还未被关闭。为了处理资源回收的问题,可以有两个选择:

  • 使用 finally 子句,把释放资源的代码放在里面

  • 使用带资源的 try 语句

 

finally

不管是否有异常被捕获,finally 子句中的代码一定会被执行:

InputStream in = new FileInputStream(...);
try {
    // might throw exceptions
} catch (IOException e) {
    // 'catch' block is not neccessary 
} finally {
    in.close();
    // other things that must be done 
}

注意:

  • 当 finally 块包含 return 语句时,会覆盖原本的值

  • 当 finally 块中也抛出异常时,会覆盖原始的异常(如果有)

 

解耦合

为了提高代码的清晰度,可以这么做:

InputStream in = ...;
try {         // --> 确保异常被处理
    try {     // --> 确保资源被释放
        ...
    } finally {
        in.close();
    }
} catch (IOException e) {
    ...
}

 

try-with-resources

如果 try 块中用到的资源属于一个实现了 AutoCloseable 接口的类,这个接口有一个 close 方法,那么就可以使用带有资源的 try 语句:

try (Scanner in = new Scanner(new FileInputSream("...")),"UTF-8") {
    while (in.hasNext()) {
        // ...
    }
}

这个 try 块退出时(不管有没有异常出现),都会自动调用 in.close(),而不需要像之前一样放在 finally 块中。 还可以指定多个资源:

try (Scanner in = new Scanner(...); 
    PrintWriter out = new PrintWriter(...)) {
    // ...    
}

使用带资源的 try 语句还有一个好处:如果 try 块和 close 方法都抛出了异常,那么 close 抛出的异常会被抑制,并自动通过 addSuppressed 方法增加到原来的异常,之后可以调用 getSuppressed 方法查看它。

 

API - java.lang.Throwable
  • getMessage

  • initCause

  • getCause

  • addSuppressed

  • getSuppressed

  • printStackTrace

  • getStackTrace

 

五、堆栈轨迹

 

堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。Throwable 类有一个 printStackTrace 方法可以访问隐式参数(调用者)的堆栈轨迹的文本信息:

Throwable t = new Throwable();
StringWriter out = new StringWriter();
// prints object t and its backtrace to the specified print stream
t.printStackTrace(out);
String description = out.toString();

还有一种更灵活的方法是使用 getStackTrace,它会得到 StackTraceElement 对象的一个数组,便于在接下来进行分析:

Throwable t = new Throwable();
StackTraceElement[] stackTraceElements = t.getStackTrace();
for (StackTraceElement element : stackTraceElements) {
    // to analyze
}

 

API - java.lang.StackTraceElement
  • getFileName

  • getLineNumber

  • getClassName

  • getMethodName

  • isNativeMethod

 

转载于:https://www.cnblogs.com/zzzt20/p/11544026.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值