这篇文章将为大家详细讲解有关Java中如何对异常进行处理,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。
目前创新互联公司已为成百上千的企业提供了网站建设、域名、网页空间、网站托管、服务器租用、企业网站设计、沧源网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。
异常及异常处理
什么是异常?总结为一句话就是:程序在执行过程中产生的异常情况。当某些事情出现了错误异常就会发生,比如打开一个并不存在的文件、尝试在一个为null的对象上调用方法等等,都会发生异常。
异常是不可预知的,可是一旦它发生了就需要进行异常处理,可谓知错就改善莫大焉!异常处理是一种错误处理机制,如果你不对异常做任何处理,异常将会导致应用程序崩溃。
一旦你选择了进行处理异常,也就意味着你承认问题的发生,采用必要要的措施去让应用程序从错误中恢复,从而让业务继续进行,阻止应用程序崩溃。
实际上异常处理并不是处理问题的唯一一种方式,如今的高级语言一般都有异常处理机制,但比较古老的如C语言是通过返回错误码的方式来处理异常的。比如数组越界比较常用的返回值是-1。
这种方式的优点是代码逻辑易于推理,没有中断和代码跳转。另一方面,这种处理方式鼓励函数的调用者总是检查返回的错误码。但是这种检查容易造成代码污染,导致代码的可读性和可维护性降低。
错误代码的另一个严重的缺点是缺乏上下文信息,你可能知道错误码“-5”代表找不到文件,但究竟找不到哪个文件呢!错误码就无法表述了。
错误代码一般用于面向过程的语言,对面向对象的高级语言,有些场景是无能为力的,比如构造函数异常,是无法返回错误码的。
异常处理
当异常被抛出时,应用程序的流程就会被中断,如果没能及时处理异常,应用程序将崩溃。用户将看到异常信息,但那些信息大多他们是看不懂的,这将是一个很糟糕的用户体验,实际上异常信息还可以包装成非常友好的提示。
所以必须进行异常处理,哪怕是为了提高用户体验、记录问题日志、优雅地退出应用程序等。
我们可以使用代码块(try...catch...)来进行异常处理,当发生异常,通过try执行代码,如果发生异常,应用程序的流程将转移到catch中,catch捕捉到异常并进行必要的处理。
以上表述的异常处理原理对初学者依然比较抽象,我们来举个例子
package com.zqf;
public class App
{
public static void main(String[] args){
System.out.println("First line");
System.out.println("Second line");
System.out.println("Third line");
//初始化具有3个元素的素组
int[] myIntArray = new int[]{1, 2, 3};
print4thItemInArray(myIntArray);
System.out.println("Fourth line");
System.out.println("Fith line");
}
private static void print4thItemInArray(int[] arr) {
//获取第4个(下标3)元素,因为没有所以抛出异常
System.out.println(arr[3]);
System.out.println("Fourth element successfully displayed!");
}
}
分析下这个程序,在main中初始化有3个元素的数组,把这个数组传递给私有方法print4thItemInArray,在print4thItemInArray中试图获取数组的第4个元素,由于没有第4个元素将抛出“ArrayIndexOutOfBoundsException”异常,应用程序只会打印到“Third line”。
执行应用输出结果如下
First line
Second line
Third line
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at com.zqf.App.print4thItemInArray(App.java:20)
at com.zqf.App.main(App.java:14)
Process finished with exit code 1
现在我们修改下,增加异常处理
package com.zqf;
public class App {
public static void main(String[] args) {
System.out.println("First line");
System.out.println("Second line");
System.out.println("Third line");
//初始化具有3个元素的素组
int[] myIntArray = new int[]{1, 2, 3};
try {//捕捉异常
print4thItemInArray(myIntArray);
}catch (ArrayIndexOutOfBoundsException ex){//异常处理
System.out.println("Have no four items!");
}
System.out.println("Fourth line");
System.out.println("Fith line");
}
private static void print4thItemInArray(int[] arr) {
//获取第4个(下标3)元素,因为没有所以抛出异常
System.out.println(arr[3]);
System.out.println("Fourth element successfully displayed!");
}
}
现在运行看看输出
First line
Second line
Third line
Have no four items!
Fourth line
Fith line
实际上这次的异常依然会发生,因为第4个元素的确不存在,所以在"Fourth element successfully displayed!"输出之前就抛出了异常,中断执行流程,但流程跳转到catch语句块了,catch只打印了一条“Have no four items”,继续向下执行。
Java异常体系
在Java中,所有的异常都有一个共同的祖先Throwable,它有2个子类:Exception(异常)和Error(错误),它们又各自有大量的子类。Exception(异常)和Error(错误)的共性和区别:两者都可以被捕捉,但前者可以被应用程序本身处理,后者是严重的,是无法恢复处理的。
最佳实践
我们经常在try语句块使用资源,比如InputStream,使用完后需要关闭。经常犯的错误是在try语句块中关闭资源。如
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// 使用inputStream读取文件
// 不要这样做
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
这种方式看似非常完美,不会有异常抛出,所有的语句在try中执行,关闭IputStream释放资源。但试想一下:如果在“inputStream.close()”语句之前就抛出异常,会怎样呢?正常的流程会被中断并跳转,导致InputStream根本没关闭。
因此,应该把清理资源的代码放在finally或try-with-resource语句中。不管是正常执行完try语句块,还是异常处理完毕,都会执行finally语句块,而你需要确保在finally关闭所有打开的资源。
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// 使用inputStream读取文件
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
log.error(e);
}
}
}
}
在JDK7引入了try-with-resource的语法,简单来说当一个资源对象(如InputSteam对象)实现了AutoCloseable接口,那么就可以在try关键字后的括号里创建实例,当try-catch语句块执行完毕后,会自动关闭资源,代码也会简洁许多。如下
public void doNotCloseResourceInTry() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file)) {
// 使用inputStream读取文件
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
你抛出的异常越具体越好,不熟悉你代码的同事或者几个月之后的你,可能需要调用你这些方法并进行异常处理,所以尽可能多的提供信息,让你的API更容易理解,比如能用NumberFormatException就不要用 IllegalArgumentException,绝对避免直接使用不具体的Exception类。
//不建议
public void doNotDoThis() throws Exception { ... }
//建议
public void doThis() throws NumberFormatException { ... }
只要你在方法声明异常,就需要做好Javadoc的注释。这点和上一条最佳实践有相同的目标:提供给调用者尽可能多的信息,便于避免异常或进行异常处理。所以请确保你在Javadoc中添加了"@throws"声明,并且描述了造成异常的情况。
/**
* This method does something extremely useful ...
*
* @param input
* @throws MyBusinessException if ... happens
*/
public void doSomething(String input) throws MyBusinessException { ... }
这条最佳实践和前面两条有点相似,但这条提供的信息不单是给方法调用者看的,而更多的是为了给记录日志或监控工具提供的,便于排查异常。实际上一般的异常类名就已经描述了问题的类型,你不必提供大量的附加信息,简洁凝练即可。比如NumberFormatException,当java.lang.Long构造函数抛出异常时会提供一句简短且清晰的文本来描述。
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
}
NumberFormatException 的名字就已经告诉你异常的种类,它携带的信息仅告诉你提供的字符串会导致异常,但如果异常名字不能表达异常种类,就需要提供更多的信息。上述的异常信息如下
17:17:26,386 ERROR TestExceptionHandling:52 - java.lang.NumberFormatException: For input string: "xyz"
如果你仔细看下JDK的源码,就会清楚java.lang.Long在构造器中做了各种校验,当某些校验失败会调用NumberFormatException.forInputString,而静态方法forInputString会把java.lang.Long的构造参数格式化后再构造一个新的NumberFormatException实例并抛出
/**
* Factory method for making a NumberFormatException
* given the specified input which caused the error.
*
* @param s the input causing the error
*/
static NumberFormatException forInputString(String s) {
return new NumberFormatException("For input string: \"" + s + "\"");
}
很多IDE都会帮助你进行最佳实践,如果你先捕捉父类异常再捕捉子类异常,它们会告诉你后面的代码不可到达或者警告已经被捕捉,因为是按照catch在在代码中顺序执行的。
所以如果先捕捉IllegalArgumentException,将不能捕捉到其子类NumberFormatException,因此最佳时间是总是先捕捉更多信息的异常(子类),再捕捉父类。如
public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
从Java异常体系的图中可知,Throwable是所有异常(Exception)和错误(Error)的祖先,Throwable是可以被捕捉,但请不要捕捉。如果你捕捉了Throwable,那么不仅仅是捕捉了异常,还捕捉了错误。但错误是无法恢复,它是被JVM抛出的严重错误,应用程序对这类错误是无能为力的。
//不要捕捉Throwable
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
你是否记得曾几何时,在分析bug时遇到代码只执行了前半部分,但却不知为何。有些开发者经常捕捉了异常,但凭经验认为异常决定不可能发生,导致没有做异常处理。
public void doNotIgnoreExceptions() {
try {
// do something
} catch (NumberFormatException e) {
// this will never happen,I'm sure!!
}
}
实际上在大多数情况下它都发生了,因为随着时间和业务逻辑的变更,try代码块的内容变更了,导致了异常发生,而你的自信不仅害了你也害了后来人。建议catch中至少要留一条日志,来告知异常问题,方便排查。
public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: "
+ e + ",but I get a mistake!");
}
}
“不要在仅仅记录日志后向上抛出异常”,这是最佳实践中最容易被忽视的一条。你会发现在大量的代码片段,甚至类库中经常捕捉异常、记录日志,然后抛出异常。
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
直观的感觉是记录异常,然后抛出异常让调用者可以恰当的处理,但同一个异常多处日志记录,会让人迷惑,请参考第4条最佳实践:简洁凝练。
17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
如果你真的需要给调用者提供更多信息,可以参考下一条最佳实践:包装异常。
比较可取的做法是捕捉到标准异常,根据实际业务自定义包装异常再向上抛出。在包装异常时通常把原始异常作为构造参数传进来,否则会丢失栈的跟踪信息,造成分析困难。
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
关于Java中如何对异常进行处理就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。