为什么有异常机制
假如在一个Java程序运行期间出现了一个错误,这个错误可能是由于文件包含了错误的信息,或者由于网络连接出现超时,或者就因为使用了无效的数组下标,或者试图使用一个没有赋值的对象(null)造成的。
当这些错误出现的时候,我们希望程序可以返回到一种安全的状态或者允许用户保存所有操作的结果,并且以妥善的方式终止程序。但是要做到这些事情,并没有我们想象中的那么简单,因为检测或者引发这个错误的代码通常离错误的源头较远。
这个时候就会用到我们的异常机制来去处理这些问题,它的原理是将控制权从错误产生的地方转移到能够处理这种情况的错误处理器。
Java中的异常
Java异常是Java提供的一种识别及响应错误的一致性机制。
Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。
异常的分类
所有的异常都是由Throwable
继承来的,它是所有异常的父类,在下一层级被分为了Error
和Exception
。
Error
描述了Java运行时系统的内部错误或资源耗尽错误,如果出现了这种错误,我们能做到的只能是给通报给用户,然后尽力的去止损,其他我们并不能做到什么。这种情况很少出现。
Exception
又被分为了RuntimeException
和非RuntimeException,区分的界限在于由程序错误导致的异常属于RuntimeException
;而程序本身没有问题,问题出在外部环境(比如IOException
)的这类异常属于其他异常。
举个栗子:
RuntimeException:
- 错误的类型转换(
ClassCastException
) - 数组访问越界(
IndexOutOfBoundsException
) - 空指针异常(
NullPointerException
)
CheckedException:
- 输入、输出流异常(
IOException
) - 数据库操作异常(
SQLException
) - 用户自定义异常
这里有一条比较有用的准则:“如果出现了RuntimeException,那么一定是你的问题”。
Java语言规范将派生于Error
类或RuntimeException
类的所有异常称为非受查异常(unchecked
)异常,所有其他的异常被称为受查(checked
)异常,这里需要注意的是,Java的编译器会检查所有的受查异常是否提供了异常处理器。
抛出
在遇到异常的时候,抛出异常的这个方法不仅要告诉编译器返回值,还要告诉编译器有可能发生什么错误,但是在我们自己编写方法的时候,不必将所有可能抛出的异常都进行声明,至于什么时候需要在方法中用throws子句声明异常,什么异常必须使用throws子句进行声明,需要记住以下四种情况:
- 调用一个抛出受查异常的方法,例如:FileInputStream构造器。
- 程序运行过程中发现错误,并且利用throw子句抛出一个受查异常。
- 程序出现错误
- Java虚拟机和运行时库出现的内部错误。
在这四种情况当中,如果出现前两种情况时,必须要告诉调用这个方法的程序员有可能抛出异常,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制,要么就应该避免发生,如果方法没有声明所有可能发生的受查异常,编译器就会发出一个错误信息,这段程序就无法通过编译。
Demo:
//1. 抛出一个
public void download(String fileName) throws IOException{
...
}
//2. 抛出多个
public void read(String fileName, String targetFileName) throws IOException, EOFException {
}
那么,具体在什么时候抛出异常,如何进行抛出,如果需要对异常的位置有一个准确的判断后并抛出,可以使用以下的方法:
public void read(Scannner in) throws EOFException{
while(...) {
if (!in.hasNext()){
if(n < len) {
throw new EOF exception
}
}
}
}
如果我们需要抛出的这个异常类是一个已经存在的异常类,我们只需要找到一个合适的异常类,创建这个类的一个对象,然后将这个对象抛出即可。
但是,我们刚刚提到过,由于一些特殊的业务需求,我们可能会去定义自己的异常类,那么我们该如何去定义一个自己的异常类呢?
@SuppressWarnings("ALL")
class FileFormatExcetion extends IOException {
public FileFormatExcetion(){}
public FileFormatExcetion(String gripe){
super(gripe);
}
}
然后我们就可以去抛出自己定义的异常类型,当然,定义一个自己的异常类型并没有这么简单,后续我会项目中给大家展示如何去定义一个自己的异常类。
public void read(String fileName) throws FileFormatException{
}
捕获
到目前为止,我们已经知道如何抛出一个异常,这个过程很容易,只要将这个异常抛出即可,但是,我们也不能一味的去抛出,如果一些运行时的错误完全可以在我们的控制之下,比如数组下标引发的错误,就应该将更多的时间花费在完善自己的代码上。
当然,也存在一些情况,我们必须将这个异常捕获,如果某个异常的法还是呢过没有在任何的地方进行捕获,那么程序就会停止执行,并且在控制台打印出异常信息,其中包括异常的类型和堆栈的内容,但是这些信息对于我们的用户来说未免过于专业,作为非专业人员,他们很可能无法理解这些代码的意思,从而对我们的产品产生一种畏惧心理,严重的影响了产品的体验。
下面是一个捕获的简单的例子:
public void read(String fileName) {
try {
InputStream in = new FileInputStream(fileName);
int b;
while ((b = in.read()) != -1) {
System.out.println(b);
}
} catch (IOException io) {
io.printStackTrace();
}
}
如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么
- 程序将跳过try语句块中的其余代码
- 程序将执行catch子句中的处理器代码
- 如果在try语句的代码中没有抛出任何异常,那么程序将会跳过catch子句
- 如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会立即退出
这里需要注意,我们也可以不对这个异常进行捕获,而是使用throws
直接抛出这个异常,让调用这个方法的程序员去处理这个异常,但是如果我们使用了throws说明符,编译器会严格执行它,如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递。
通常来说,我们应该捕获的是那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递,如果想传递一个异常,就必须在方法的首部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。
当然,也有例外的情况,如果编写一个覆盖父类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个受查异常,不允许在子类的throws说明符中出现超过父类方法所列出的异常类范围。
如果我们需要捕获多个异常,可以尝试使用以下的语法结构:
//这样
try{
//code
} catch (FileNotFoundException e) {
//handle1
} catch (UnknownHostException e) {
//handle2
} catch (IOException e) {
//handle3
}
//如果两个异常之前的处理动作一致可以是这样
//但是需要注意的是,这里的异常变量隐含为final变量,不能在子句中为e赋不同的值
try {
//code
} catch (FileNotFoundException | UnknownHostException | IOException e) {
//handle
}
如果我们需要获取异常的更多信息,可以使用:
//获取详细的错误信息
e.getMessage()
//获取异常对象的实际类型
e.getClass.getName()
下面对于异常的抛出有一个小小的知识点教给大家,我们通常会定义一些自定义抛出异常,这些异常的描述通常比较通俗易懂,但是对于开发人员来说,我们需要知道问题的所在,这时这个小技巧就可以起到一个很好的作用:
//抛出
catch (SQLException e) {
Throwable se = new ServletExcetion("数据库错误");
se.initCause(e);
throw se;
}
//获取
Throwable e = se.getCause();
这种包装技术,既可以让用户抛出子系统中的高级异常,而且不会丢失原始异常的细节。
如果在一个方法中发生了一个受查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个受查异常,并把它包装成一个运行时异常。
finally子句
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收,那么就会产生资源回收问题。一种解决方案是捕获并重新抛出所有的异常。但是,这种解决方案比较乏味,这时因为需要在两个地方清除所分配的资源。一个在正常代码中,一个在异常代码中。
当然,Java给我们提供了一种更好的解决方案,那就是finally子句,下面我们来看一个例子,之后你就会对finally语句有一个非常清晰地认识:
try{
//1
//2,这里也许会抛出一个异常
} catch (Exception e){
//3
show error message
//4
} finally {
//5
}
//6
在以上的这段的代码中,在已下3种情况下回执行finally:
- 代码没有抛出异常,这时程序会按照 1. 2. 5. 6的顺序执行
- 抛出一个在catch子句中捕获的异常,这时会分为两种情况,第一种情况是:如果在catch子句没有抛出异常,程序将执行try语句块之后的第一条语句,也就是说会按照 1. 3. 4. 5. 6的顺序去执行;第二种情况是:如果在catch子句中抛出了异常,异常将会炮灰这个方法的调用者,这里会执行 1, 3 ,5处的语句
- 代码跑出了一个非catch子句捕获的异常,这种情况下,会执行1. 5处的语句。
通常我们会在关闭资源或者IO流的时候去使用这个finally,以免因为异常而导致内存溢出,服务崩溃。
这里需要注意一点,当try语句和finally语句中含有return语句的时候,在方法返回前,finally子句的内容会被执行,而且,如果在finally子句中也有一个return语句,这个返回值将会覆盖原始的返回值。
当我们使用带资源的try语句时,使用finally也许会造成一个异常被覆盖的问题,即try语句块中会抛出某个异常,执行finally语句块中跑出了同样的异常,这样的话就会导致原有的异常会丢失,转而抛出的finally语句中的异常。这时我们可以使用带资源的try语句来处理(前提是这个资源实现了AutoCloseable接口的类):
try(Resource res = ...) {
//TODO:res run
}
这里的try块在退出的时候,会自动的去调用res.close(),这样做即实现了finally的效果,又可以将原有代码块的异常抛出,而抑制close方法抛出的异常
Tips
异常的知识,到这里就告一段落了,最后告诉大家几点需要注意的事项,在以后的使用中可以更加的得心应手:
- 异常处理不能代替简单的测试!,只有在异常情况下使用异常机制,因为捕获异常需要耗费大量的时间和资源。
- 不过过分的细化异常(注意粒度)
- 利用异常层次结构。
- 不要压制异常
- 在检测错误时,“苛刻”要比放任来的更加合适(早抛出)
- 不要羞于传递异常(晚捕获)
公众号
扫码或微信搜索 Vi的技术博客,关注公众号,不定期送书活动各种福利~