Java编码中异常的使用建议

Code

异常,精确计算机世界里的不安定份子。当然说的是异常的使用,似乎没有什么统一的标准。但是我觉得在一个团队或者是系统里面还是应该遵循一定的规矩。

无规矩不方圆。

这周就把异常的使用给列一个使用准则。

首先看下Java异常的分类。

Java 异常的分类

  • 受检异常 (Checked Exception). 异常的处理由编译器来保证,如果方法声明异常但是没处理则编译失败。
  • 非受检异常 (Unchecked Exception). 编译器不会检查的一类异常
    • RuntimeException. IndexOutOfBoundsException, IllegalArgumentException 等
    • Error. 表示很严重的问题,应用自己也搞不定, 与代码编写者无关。 OutOfMemoryError 等。 不要继承Error来定义异常

异常机制提供了一种异常事件的冒泡处理机制,你可以选择你想处理这个异常的层次。 如果没有异常的话,你就要在很低的层次处理异常,并想办法通知上层

异常使用中的困惑

在我的使用或者团队大家的共识是,异常使用困惑的地方主要集中在下面两点:

  • 什么时候使用受检异常,什么时候使用非受检异常
  • 错误码和异常的使用

所以接下来主要围绕着两个困惑来进行解答

先来看看受检和非受检异常使用的时机,此处非受检指的是 RuntimeException

受检异常的使用时机

Use checked exceptions for conditions from which the caller
can reasonably be expected to recover

  • 当可以用来表示业务流转中的异常分支,给出更合理的错误提示时
  • 当你觉得调用方可以解决异常时
  • 当异常由外部不可控的因素导致时,例如用户输入数据库异常文件不存在网络

当你面临上面的情景时可以采用受检异常

非受检异常的使用时机

Use runtime exceptions to indicate programming errors (Bugs) - From Effective Java

RuntimeException 通常指程序运行时的错误,可以引申为调用方错误的使用了API或者类库设计者的问题。

Runtime exceptions can occur anywhere in a program, and in a typical one they can be very numerous. Having to add runtime exceptions in every method declaration would reduce a program’s clarity. Thus, the compiler does not require that you catch or specify runtime exceptions - From Oracle The Java™ Tutorials

上面说到RuntimeException是程序中非常常见的错误,因此没有强制让编译器来检查这一类错误。但是程序员自己就需要做好预先检查的工作。RuntimeException 有个好处就是能够穿透到最上层,让最上层做决定。因此可以围绕下面几点来使用RuntimeException

  • 当你处理不了底层声明的异常时,将底层异常转换为RuntimeException。
  • 当用户调用的参数不符合程序预期,使用不当时

    一个强有力的证明: IllegalArgumentException 定义的是 RuntimeException

错误码和异常使用的时机

在没有异常机制的程序语言中,可能比较依赖错误码等来作为业务层面的流程判断。但是在Java中提供了异常的处理机制,这种冒泡处理机制,让你可以选择你想处理这个异常的层次。 如果没有异常的话,你就要在很低的层次处理异常,并想办法通知上层。

总结来讲,错误码和异常的特点就是

  • 异常是强类型且类型安全的分支控制技术
  • 返回错误值则是弱类型不安全

作为Java程序员,应该优先使用异常来反馈程序中的异常情况。

使用异常来区分正常的业务场景和错误的业务路径

1
2
3
4
5
6
7
8
9
10
11
12
public String foo() IOException, FileNotFoundException {
...
}

// 一个好的使用方法应该是这样
try {
foo();
} catch (IOException ioe) {
// 给出友好的提示信息
} catch (FileNotFoundException fe) {
// 给出友好的提示信息
}

一种异常和错误码的混合方案

定义一个业务异常,然后异常中添加字段来表示错误码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
enum ErrorCode {
INVALID_PARAM, INVALID_PATH, INVALID_STAUS;
}

class MyException extend Exception {
public MyException(String msg) {
super(msg);
}
}

// 使用场景
public String foo() throws MyException {
if (...) {
throw new MyException(ErrorCode.INVALID_PARAM.getName());
}

...

if (...) {
throw new MyException(ErroCode.INVALID_PATH.getName());
}

...

if (...) {
throw new MyException(ErrorCode.INVALID_STATUS));
}
}

错误码和异常使用的建议

  • 同构系统里优先使用异常来表示错误。
    • 比如两个Java类相互调用,方法之间就可以用异常来处理非正常情况,这样可以充分发挥异常类型的作用
  • 异构系统的情况使用返回错误信息的方式。
    • 比如提供一个webservice接口,就可以用不同的业务编码来表示错误情况

最后是异常使用的一些实践

异常处理的实践

  • 记得释放资源。特别是数据库还有网络资源,使用 try-catch-finally
  • 不要使用异常作控制流程之用
  • 不要忽略异常
  • 生产代码不要 printStatckTrace()
  • 尽量缩小异常的捕获范围

具体来讲就是

try-catch-finally 释放资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
OutputStreamWriter out = ... 
java.sql.Connection conn = ...
try {
  ...
} catch(SQLException sqlex) {
  ...
} catch(IOException ioex) {
  ...
} finally {
  if (conn != null) {
   try {
    conn.close();
   } catch(SQLException sqlex2) {
    ...
   }
  }

  if (out != null) {
   try {
    out.close();
   } catch(IOException ioex2) {
    ...
   }
  }
}

不要将异常用作控制流(if-else)

虽然异常本身具有流程控制的属性,但是直接作为类似 if-else这样的方式来使用还是不应该的

1
2
3
4
5
6
7
8
9
10
11
12
public void check(String filePath) FileNotFoundException, FileExistException  {
...
}

// 使用
try {
check();
} catch (FileNotFoundException fne) {
// do-file not found things ..
} catch (FileExistException fee) {
// do-file found things
}

不要忽略异常

  • 可以只打个日志,但是不要什么都不做
  • 如果你什么都做不了,就不要抓或者转换为RuntimeException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException ex) {} //忽略的异常,这样要不得。出了问题坑自己、坑队友

// 接收异常
try {
...
} catch (Exception e) { // 过分泛华的异常,不利于排查问题
...
}

// 抛出异常
try {
...
} catch (IOException ioe) {
throw new Exception(ioe); // 泛化了异常, 外层调用丢失了异常类型的优势
}

// 自定义异常
try {
...
} catch (SqlException sqle) {
throw new MyOwnException(); // 定义了新的异常,但是丢了原始异常信息
}

生产代码不要 printStackTrace();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 不好的方式
try {
...
} catch (IOException e) {
e.printStackTrace();
}

try {
...
} catch (IOException e) {
logger.error("message here" + e);
}

try {

} catch (IOException e) {
logger.error("message here" + e.getMessage());
}

// 比较好的方式
try {
...
} catch (IOException e) {
logger.error("message here", e);
}

异常的捕获范围

  • 循环的场景,注意try代码块的范围
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bad case
try {
while(rs.hasNext()) {
foo(rs.next());
}
} catch (SomeException se) {
...
}

// good case
while(rs.hasNext()) {
try {
foo(rs.next());
} catch (SomeException se) {
...
}
}

参考资料

http://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html

http://stackoverflow.com/questions/6115896/java-checked-vs-unchecked-exception-explanation

http://www.javapractices.com/topic/TopicAction.do?Id=129

http://www.ibm.com/developerworks/cn/java/j-lo-exception/index.html

http://www.blogjava.net/freeman1984/archive/2013/07/26/148850.html