Java异常处理框架是非常强大并且很容易理解和使用,异常可能产生于不同的情况,例如:用户错误数据的输入,硬件故障,网络连接失败,数据服务器故障等等,下面我们需要学习在java中如何处理这些异常。
在程序执行的时候,无论什么时候产生错误,都会创建一个Exception对象并且正常的程序流也会中断,JRE会尝试找到处理这个异常的处理者。一个异常对象包含了许多的调试信息,例如:方法层次、产生异常的行号、异常类型等等。当异常在一个方法中产生,创建这个异常对象并且将它提交给运行环境的过程叫做抛出异常。
一旦运行环境接收到这个异常对象,它会尝试找到这个异常的处理者,这个处理者就是一个处理这个异常的代码块,寻找异常处理者的过程是非常简单的:它首先会搜索这个产生异常的方法,如果没有合适的异常处理者,它就会查找这个方法的调用者,依次推进。所以,如果方法调用栈是A–>B–>C,如果异常在方法C中产生,那么搜索异常处理者的过程就是C–>B–>A,如果合适的异常处理者被找到,就会把这个异常对象传递给这个异常处理者进行处理,如果没有找到合适的异常处理者,异常信息就会在应用终端打印处理。
在java程序中,我们会使用指定的关键字去创建一个异常处理块,下面我们会讲到。
异常处理关键字
为了能够对异常进行处理,java里面提供了指定的关键字。
1、throw
throw是为了抛出异常给java运行环境,让它进行处理
2、throws
如果在一个方法中产生了一个异常并且我们不想对其进行处理,我们就在方法上使用throws关键字,目的是为了让这个方法的调用者知道这个方法可能产生异常,这个方法的调用者可以处理这个异常也可以使用同样的方法来告诉上层的调用者。
3、try-catch
try-catch是处理异常的代码块。
4、finally
finally块只能跟try-catch块一块使用,由于异常中断了程序的执行,这样我们有一些资源可能被打开了但是还没来得及关闭,这样就可以使用finally块,不管会不会产生异常finally块都会被执行。
下面来举个例子来看看异常的处理。
package com.journaldev.exceptions;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionHandling {
public static void main(String[] args) throws FileNotFoundException, IOException {
try {
testException(-5);
testException(-10);
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch(IOException e) {
e.printStackTrace();
} finally {
System.out.println("Releasing resources");
}
testException(15);
}
public static void testException(int i) throws FileNotFoundException, IOException{
if(i < 0) {
FileNotFoundException myException = new FileNotFoundException("Negative Integer "+i);
throw myException;
} else if(i > 10){
throw new IOException("Only supported for index 0 to 10");
}
}
}
程序输出如下:
java.io.FileNotFoundException: Negative Integer -5
at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24)
at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10)
Releasing resources
Exception in thread "main" java.io.IOException: Only supported for index 0 to 10
at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27)
at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)
testException(-5)执行后,产生FileNotFoundException异常,程序流被中断,这样try块后面的语句testException(-10)就不会被执行,因为不管是否产生异常,finally块总会被执行,所以打印出了Releasing resources,另外,我们也可以看到finally块后面的语句testException(15)会被继续执行,它抛出了异常,但是没有被捕捉,而是使用throws语句,交给了它的调用者。另外,printStackTrace()是Exception中一个非常有用的方法,在调试的时候可以使用。
下面对上面的内容进行了一些总结:
1、如果没有try块,就不可能有catch和finally块。
2、try块的后面应该有catch块或者finally块,也可以同时存在。
3、在try-catch-finally块之间,不能写任何代码。
4、一个try块的后面可以有多个catch块。
5、try-catch可以内部进行包含,类似于if-else语句,if-else内部可以包含if-else。
6、一个try-catch块的后面只能有一个finally块。
Exception层次
我们知道,当产生任何的异常都会创建一个异常对象,Java异常是有层次的,它们使用继承关系来对不同类型的异常进行分类。Throwable是整个继承层次的父类,它有两个子对象:Error和Exception,其中Exceptions又分为可检查异常(checked exceptions)和运行时异常(runtime exception)。
1、Errors:
他表示异常的发生场景超出了应用范围,它无法预测也无法修复,例如,硬件故障、JVM崩溃或者内存溢出。这也是为什么我们有一个单独的errors层次,我们不应该对其进行处理,一些普通的错误(Errors)包括内存异常(OutOfMemoryError)和栈溢出(StackOverflowError)。
2、Checked Exceptions:这种异常是我们程序可以预测并且进行修复的,例如:FileNotFoundException。我们应该捕获这种异常并且给用户提供有用信息,另外为了方便调试,可以适当的打印出错误信息。Exception是所有可检查异常(Checked Exceptions)的父类,如果我们抛出了一个可检查的异常,我们必须在这个方法中对其进行捕获或者使用throws关键字把它抛给调用者。
3、Runtime Exception:运行时异常是程序员在编写代码时不好的编码习惯或者错误的使用造成的,例如从数组中取元素的时候,我们应该首先检查数组的长度,然后再去进行数组元素的提取,因为在运行时可能抛出ArrayIndexOutOfBoundException数组访问越界异常。运行时异常可以不需要进行捕获,也不需要在方法上添加throws关键字来标识,因为如果你有良好的编码习惯,这些异常是可以避免的。
Exception中有用的方法
Exception和所有它的子类并没有提供具体的方法,所有的方法是是定义在它的基类Throwable里面。异常根据不同的异常类型进行创建,这样我们就可以很容易的找出根本原因并且通过这些类型处理异常。为了交互的目的,Throwable实现了Serializable接口。
下面列出了Throwable里面的一些有用的方法:
public String getMessage()
这个方法返回Throwable的消息字符串,当通过构造者创建异常对象的时候会提供这个消息字符串,我们自己在创建异常的时候,会传入一个字符串,这个就是消息。
public String getLocalizedMessage()
子类可以重写这个方法,为调用它的程序提供指定的消息。Throwable实现这个方法仅仅通过getMessage()去返回异常消息。
public synchronized Throwable getCause()
这个方法返回异常的原因,如果是空id就说明这个异常未知。
public String toString()
它将Throwable以字符串的形式返回,主要包括Throwable类名和本地消息。
public void printStackTrace()
这种方法打印堆栈跟踪信息到标准错误流,这种方法重载,我们可以传递的PrintStream或PrintWriter的作为参数以文件或流的形式打印堆栈跟踪信息。
Java 7自动资源管理和Catch块的完善
如果你在一个单独的try块中捕获许多的异常,那个这个catch块的代码可能会看起来丑陋并且包含了许多额外的代码去打印错误,在java 7中,在一个单独的catch块中可以捕获许多异常,例如:
catch(IOException | SQLException | Exception ex){
logger.error(ex);
throw new MyException(ex.getMessage());
}
具体的用法可以看看:Java 7 Catch Block Improvements
许多时候,我们使用finally块来关闭资源,有时我们如果忘记了关闭它们,这样当资源被耗尽的时候可能就会出现运行时异常。这种异常很难进行调试,我们需要检查所有使用这种资源类型的地方,确保是否关闭。在Java 7中,我们创建一个资源在try的声明中,并且使用这个资源在try-catch块里面,当运行到这个try-catch之外的时候,运行环境会自动关闭这些资源,例如:
try (MyResource mr = new MyResource()) {
System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
e.printStackTrace();
}
具体的用法参考这篇文章:Java 7 Automatic Resource Management
创建自定义的异常类
java中提供了很多我们可以使用的异常,但是有时我们对指定的带有适当的信息以及我们需要跟踪的自定义字段的异常,需要通过创建自定义的异常类去通知调用者。例如:我们写了一个方法只去处理文本文件,因此当其他类型的文件输入的时候,我们需要将对应的错误代码提供给调用者。
下面有一个例子:
MyException.java
package com.journaldev.exceptions;
public class MyException extends Exception {
private static final long serialVersionUID = 4664456874499611218L;
private String errorCode="Unknown_Exception";
public MyException(String message, String errorCode){
super(message);
this.errorCode=errorCode;
}
public String getErrorCode(){
return this.errorCode;
}
}
CustomExceptionExample.java
package com.journaldev.exceptions;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class CustomExceptionExample {
public static void main(String[] args) throws MyException {
try {
processFile("file.txt");
} catch (MyException e) {
processErrorCodes(e);
}
}
private static void processErrorCodes(MyException e) throws MyException {
switch(e.getErrorCode()){
case "BAD_FILE_TYPE":
System.out.println("Bad File Type, notify user");
throw e;
case "FILE_NOT_FOUND_EXCEPTION":
System.out.println("File Not Found, notify user");
throw e;
case "FILE_CLOSE_EXCEPTION":
System.out.println("File Close failed, just log it.");
break;
default:
System.out.println("Unknown exception occured, lets log it for further debugging."+e.getMessage());
e.printStackTrace();
}
}
private static void processFile(String file) throws MyException {
InputStream fis = null;
try {
fis = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new MyException(e.getMessage(),"FILE_NOT_FOUND_EXCEPTION");
}finally{
try {
if(fis !=null)fis.close();
} catch (IOException e) {
throw new MyException(e.getMessage(),"FILE_CLOSE_EXCEPTION");
}
}
}
}
异常处理最佳实践
1、使用具体的异常(Use Specific Exceptions)
异常层次的基类不能提供任何有用的信息,这也是为什么java里面有如此多的异常类,例如:IOException有子类FileNotFoundException和FileNotFoundException等,我们应该throw和catch具体的异常,这样调用者就知道产生异常的根本原因,以便及时处理。这也是调试变得更加容易同时帮助客户应用适当的处理异常。
2、尽可能早的抛出异常(Throw Early or Fail-Fast)
我们应该尝试尽可能早的把异常抛出来,看看上面processFile()方法,如果我们传递一个null参数给这个方法,我们可能得到下面的异常:
Exception in thread "main" java.lang.NullPointerException
at java.io.FileInputStream.<init>(FileInputStream.java:134)
at java.io.FileInputStream.<init>(FileInputStream.java:97)
at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42)
at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
我们在调试的过程中需要确定异常发生的位置,上面的提示并不是清楚的,如果我们实现这个逻辑去尽可能早的检查这个异常,例如:
private static void processFile(String file) throws MyException {
if(file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");
//further processing
}
上面的错误信息提示如下,可以看到它可以更清楚的显示异常发生的位置和信息。
com.journaldev.exceptions.MyException: File name can't be null
at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37)
at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
3、晚一些捕获异常(Catch Late)
由于java强制我们需要处理可检查的异常或者在方法中使用throws标识,有时候,一些程序员会去捕获异常并且打印错误信息,这种行为是不好的,因为这样调用程序就不能得到异常通知,我们应该在我们可以处理它的时候进行异常的捕获。例如,上面我们将异常抛给调用它的方法去进行处理。如果另一个程序希望使用不同的方法进行处理,它也可以得到这个异常进行处理,所以,我们应该把异常抛给它的调用者,让他们来决定如何进行处理。
4、关闭资源(Closing Resources)
异常会中断程序的执行,所以应该在finally块中关闭所有的资源或者使用java 7.0中的try-with-resources去让运行环境帮你自动关闭。
5、记录异常(Logging Exceptions )
我们应该总是记录异常信息,抛出异常提供更详细的信息。这样调用者就可以很容易知道为什么发生。我们应该总是避免仅仅为了消耗异常而不能提供有用信息的空catch块。
6、一个try块处理多个异常(Single catch block for multiple exceptions)
许多时候,我们打印异常信息并且提供异常信息给用户,这样我们可以使用java 7中的特性,在一个try块中处理多个异常。
7、使用自定义异常(Using Custom Exceptions)
自定义异常处理机制比普通的异常捕获会更加的完美,我们可以创建一个带有异常码的自定义异常并且调用程序可以处理这些错误码。这样我们可以创建一个工具方法来处理不同的错误码并且使用它。
8、命名规范和打包(Naming Conventions and Packaging)
如果我们创建一个自定义的异常,确保这个异常以Exception结尾,这样就可以很清楚的知道它是一个异常,也可以像JDK一样对它进行打包,例如:IOException是所有IO操作异常的基类。
9、恰当的使用异常(Use Exceptions Judiciously)
使用异常是有代价的,并不是在所有的地方都需要使用异常,我们可以返回一个boolean变量来告诉调用者是否操作成功,这个是相当有用的,这样操作就是可选的而且你的程序也不会因为失败而中断。
10、文档化异常的抛出(Document the Exceptions Thrown)
使用javadoc的@throws是明确的指定某个方法会抛出异常,这样非常有用的,当你提供一个接口给其他的应用去使用。
原文链接:Java Exception Handling Tutorial with Examples and Best Practices