Exception最佳实践
异常实现及分类
上图可以简单展示一下异常类实现结构图,当然上图不是所有的异常,用户自己也可以自定义异常实现。上图已经足够帮我们解释和理解异常实现了:
1.所有的异常都是从Throwable继承而来的,是所有异常的共同祖先。
2.Throwable有两个子类,Error和Exception。
其中Error是错误,对于所有的编译时期的错误以及系统错误都是通过Error抛出的。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
3.Exception,是另外一个非常重要的异常子类。它规定的异常是程序本身可以处理的异常。异常和错误的区别是,异常是可以被处理的,而错误是没法处理的。
4.Checked Exception
可检查的异常,这是编码时非常常用的,所有checked exception都是需要在代码中处理的。它们的发生是可以预测的,正常的一种情况,可以合理的处理。比如IOException,或者一些自定义的异常。除了RuntimeException及其子类以外,都是checked exception。
5.Unchecked Exception
RuntimeException及其子类都是unchecked exception。比如NPE空指针异常,除数为0的算数异常ArithmeticException等等,这种异常是运行时发生,无法预先捕捉处理的。Error也是unchecked exception,也是无法预先处理的。
上面参考与:https://blog.youkuaiyun.com/michaelgo/article/details/82790253
如何去处理异常
当我们写Java代码进行异常处理的时候往往会遇到很多一些小的问题,比如我们应该怎么处理异常,处理哪些异常,很多团队可能对异常的处理都有一些规范,但是不用的团队的规范都不一样,下面我就说一些处理异常的最佳实践。
-
不要直接抛出Exception
看下面这个代码
public class Example1 { public void test() throws Exception{ Integer number = Integer.parseInt("heloworld"); } }
这里是直接把Exception抛出,但是Exception还有很多子异常,当发生错误的时候,调用者并不能知道发生了什么错误,所以应把异常具体化,就比如上面这个例子改成下面这个样子
public class Example1 { public void test() throws NumberFormatException{ Integer number = Integer.parseInt("heloworld"); } }
点进源码可以看到, Integer.parseInt()方法抛出的是NumberFormatException异常,所以我们写的具体点,当发生异常的时候,调用者很容易就能从NumberFormatException这个单词上知道,哦,这是个数字格式化异常。
-
给异常加说明
看下面这个代码
public class Example2 { class NotFindException extends Exception { public NotFindException(String message) { super(message); } } /** * @throws NotFindException 如果发生文件找不到错误,就抛出此错误 */ public void doThings() throws NotFindException { } }
这样的话,当调用者调用doThings()方法的时候,就能知道,什么时候会抛出这个错误,出现之后如果去处理。
-
使用描述性信息抛出异常
看下面这个代码
public class Example3 { private static final Logger LOGGER = Logger.getLogger(Example3.class); public void doSomeThings() { try { new Long("helloworld"); } catch (NumberFormatException e) { LOGGER.error("数据格式化错误", e); } } }
这样,进行打印日志的时候,就能知道出现错误的原因,从而进行错误处理。而且,始终正确包装自定义异常中的异常,以便堆栈跟踪不会丢失,也就是说,上面的catch块不要这样写
LOGGER.error("数据格式化错误", e.getMessage());
-
优先捕获具体异常
我们大家都知道,在进行异常处理的时候,是找到第一个符合的异常catch块进行处理,如果把具体的异常放在后面的话,就可能出现一种不可达的情况,Java也不允许我们这样做。
如果是下面这段代码
public class Example4 { private static final Logger LOGGER = Logger.getLogger(Example4.class); public void catchMostSpecificExceptionFirst(){ try { doSomeThings(); } catch (IllegalArgumentException e){ LOGGER.error("非法字符串", e); } catch (NumberFormatException e){ LOGGER.error("字符串转换失败", e); } } }
NumberFormatException 继承了 IllegalArgumentException 类,当执行doSomeThings()方法发生了NumberFormatException 异常的时候,就会在 IllegalArgumentException异常catch块进行异常处理,从而不能获取准确的异常信息。
正确的写法是先写具体异常的catch块。
-
永远不要捕获Throwable类
看这段代码
public class Example5 { private static final Logger LOGGER = Logger.getLogger(Example4.class); public void doNotCatchThrowable() { try { doSomeThings(); } catch (Throwable e) { //永远不要这么干 } } }
这是一个更严重的麻烦。 因为这样的话,catch不光会捕获exception,还会捕获error。 error是JVM本身无法处理的不可逆转的条件。 对于某些JVM的实现,JVM可能实际上甚至不会在错误上调用catch子句。
-
不要在try块中进行资源关闭
看下面这段代码
public class Example6 { private static final Logger LOGGER = Logger.getLogger(Example6.class); public static void main(String[] args) { File file = new File("d:/1.json"); InputStream inputStream = null; try { inputStream = new FileInputStream(file); //事件1 //事件2 inputStream.close(); } catch (FileNotFoundException e) { LOGGER.error(e.toString(), e); } catch (IOException e) { LOGGER.error(e.toString(), e); } } }
假想一下,如果在try块中关闭资源,当进行事件1或者事件2操作时,抛出了异常,资源关闭方法close就不会被执行了,达不到我们想要的效果,所以正确的关闭方式应该是这样的
public class Example6 { private static final Logger LOGGER = Logger.getLogger(Example6.class); public static void main(String[] args) { File file = new File("d:/1.json"); InputStream inputStream = null; try { inputStream = new FileInputStream(file); //事件1 //事件2 } catch (FileNotFoundException e) { LOGGER.error(e.toString(), e); } finally { if (inputStream != null){ try { inputStream.close(); } catch (IOException e) { LOGGER.error(e.toString(), e); } } } } }
首先对inputStream进行非空判断,因为可能在try块中还没new一个inputstream就发生了错误,所以如果不进行非空判断就进行close操作可能就会发生一个空指针异常。
但是这样写还有一个问题,当出现多个需要关闭的资源时应该怎么写?正确的写法是在第一个资源关闭的finally块中进行第二个资源的关闭,依此类推,这样写的话,就会造成循环的嵌套,所以jdk1.7给了我们一种优雅关闭资源的一个方法,也就是try-with-resource方法。
public class Example6 { private static final Logger LOGGER = Logger.getLogger(Example6.class); public static void main(String[] args) { File file = new File("d:/1.json"); try (InputStream inputStream = new FileInputStream(file);){ //事件1 //事件2 } catch (FileNotFoundException e) { LOGGER.error(e.toString(), e); } catch (IOException e) { e.printStackTrace(); } } }
这样程序就会自动帮我们运行close()方法。
-
不要忽略异常
要对异常信息进行记录,哪怕你觉得不会出现这种错误。也就是说,不要在catch块中什么都不做,
-
要么记录异常要么抛出异常,但不要一起执行
public class Example7 { private static final Logger LOGGER = Logger.getLogger(Example4.class); public void fo(){ try { new Long("hello"); } catch (NumberFormatException e){ LOGGER.error(e.toString(), e); throw e; } } public static void main(String[] args) { new Example7().fo(); } }
正如在上面的示例代码中,记录和抛出异常会在日志文件中产生多条日志消息,代码中存在单个问题,并且让尝试挖掘日志的工程师生活变得很糟糕。
-
不要使用printStackTrace()语句或类似的方法
完成代码后,切勿忽略printStackTrace()。 你的同事可能会最终得到这些堆栈,并且对于如何处理它完全没有任何知识,因为它不会附加任何上下文信息。
-
如果你不打算处理异常,请使用finally块而不是catch块
try { someMethod(); //Method 2 } finally { cleanUp(); //do cleanup here }
这也是一个很好的做法。 如果在你的方法中你正在访问Method 2,而Method 2抛出一些你不想在method 1中处理的异常,但是仍然希望在发生异常时进行一些清理,然后在finally块中进行清理。 不要使用catch块。
-
包装异常不要丢弃原始异常
看下面一段代码
public class Example8 { class MyBusinessException extends Exception { public MyBusinessException(String message) { super(message); } public MyBusinessException(String message, Throwable cause) { super(message, cause); } } public void warpException(String id) throws MyBusinessException { try { long userId = Long.parseLong(id); System.out.println("userId " + userId); } catch (NumberFormatException e) { throw new MyBusinessException("这里是具体的描述"); } } public static void main(String[] args) throws MyBusinessException { new Example8().warpException("hello"); } }
下面看运行结果
Exception in thread "main" exception.Example8$MyBusinessException: 这里是具体的描述 at exception.Example8.warpException(Example8.java:23) at exception.Example8.main(Example8.java:28)
我们在运行结果中,并没有看到我们应该怎么快速处理这个异常信息,原因是,我们在包装异常的时候,并没有把异常栈传递进去,所以我们正常的做法是,在catch块中把栈信息也传递进去,即
throw new MyBusinessException("这里是具体的描述", e);
此时,结果如下
Exception in thread "main" exception.Example8$MyBusinessException: 这里是具体的描述 at exception.Example8.warpException(Example8.java:25) at exception.Example8.main(Example8.java:30) Caused by: java.lang.NumberFormatException: For input string: "hello" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:589) at java.lang.Long.parseLong(Long.java:631) at exception.Example8.warpException(Example8.java:22) ... 1 more
-
**不要在finally中执行return操作
public class ReturnTest {
static int x = 1;
static int y = 10;
static int z = 100;
public static void main(String[] args) {
int value = finallyReturn();
System.out.println("value= " + value);
System.out.println("x= " + x);
System.out.println("y= " + y);
System.out.println("z= " + z);
}
public static int finallyReturn(){
try {
// ...
return ++x;
} catch (Exception e){
return ++y;
} finally {
return ++z;
}
}
}
运行结果如下
value= 101
x= 2
y= 10
z=101
这样返回值就会变的不可控,所以我们不要再finally中使用return语句。
此时,我们知道怎么发生的异常,从而可以快速的去处理异常。