异常,让程序有更好的容错性,并分离了错误处理代码与逻辑实现代码

异常处理的目的:

  • 使程序代码混乱最小化
  • 捕获保留异常讯息
  • 通知合适的人员处理相应异常
  • 采用合适的方式结束异常活动

异常处理的使用规范:

  • 先小后大,先处理子类异常,在处理父类异常。

  • 异常代码结构:

1
2
3
4
5
6
7
try {
//逻辑实现代码
} catch(ExceptionName e) {
//异常处理代码
} finally {
//回收被打开的物理资源,如数据库连接、网络连接、读取磁盘文件等。
}
  • 增强try 代码结构:
    1
    2
    3
    4
    5
    6
    7
    try(
    //资源调用代码,运行完try块后自动关闭资源
    ) {
    //逻辑实现代码
    } catch(ExceptionName e) {
    //异常处理代码
    }

增强try的好处是省略了finally块来处理需要关闭的资源,在try里的资源会在try块运行结束或捕获异常后关闭。

  • 异常结构使用总结:
try 警官专抓异常
出行带着catch 探员和finally 探员
catch 探员负责定罪
finally 探员负责善后
catch finally 有时齐出动
有时带其中一个便够
两个都不带也行
函数签名带上throws
增强 try警官很好用
自带finally 防疏漏

不要滥用异常:

  • 别try 普通的业务代码(不会抛出异常的代码)
  • 别用try 取代负责业务逻辑的if 和 else 小兄弟。(杀鸡用牛刀:异常的性能比if else 差)
  • 别把try 块踹成一个胖子(避免使用庞大的try块)
  • 不要乱抓人!(尽量不要使用catch all语句)

(以下参考王垠的编程的智慧,非常好的一篇文章 和 《疯狂Java讲义》第十章关于异常处理)

正确使用异常:

  1. 道不同不相为谋,异常要各try 各的(不同的异常情况要分开try,不要潦草的用一个Exception捕获所有异常)
  2. 抓我要有明确证据(尽量减少try 块的内容,尽量分离与异常无关的代码)
  3. 抓我就要对我负责啊(调试的时候print 异常,调试完毕后要对异常进行相应处理
  4. 抓我也得选对地方吧(在合理的Layer 处理异常)
  5. 不要把我丢给不相关的人(在需要的地方进行异常转译)

案例分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Properties p = Properties();
try {
p.load(new FileInputStream("sql_config.txt");
} catch(IOException e) {
e.printStackTrace();
}
String drv = p.getProperty("drv");
String url = p.getProperty("url");
String usr = p.getProperty("usr");
String pwd = p.getProperty("pwd");
try {
Class.forName(drv);
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
try {
Connection conn = DriverManager.getConnection(url, usr, pwd);
} catch(SQLException e) {
e.printStackTrace();
}

上面的例子示范了JDBC 数据库的连接过程,此处有不同的异常情况,第一个try 块里检测的是有没有sql_config.txt这个文件,第二个try 块里检测了程序是否成功加载了SQL 驱动,第三个try 块检测了是否通过DriverManager 成功生成了Connection 实例。三个try 块分别检测了不同的异常类型,这样才达到了捕获异常的目的。千万不要用Exception 来捕获所有的异常,因为Exception 是所有异常的父类,如果一有任何异常出现就会抛出异常,而不会按代码想要catch 的异常类型检查相应异常,于是难以定位异常发生的地点以及类型,便也失去了使用异常的意义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Properties p = Properties();
try {
p.load(new FileInputStream("sql_config.txt");
//业务代码没有分离
String drv = p.getProperty("drv");
String url = p.getProperty("url");
String usr = p.getProperty("usr");
String pwd = p.getProperty("pwd");
Class.forName(drv);
Connection conn = DriverManager.getConnection(url, usr, pwd);
//捕获多个异常
} catch(IOException e1) {
e1.printStackTrace();
} catch(ClassNotFoundException e2) {
e2.printStackTrace();
} catch(SQLException e3) {
e3.printStackTrace();
}

上面的例子把三个try 块的内容放在一起,用catch 来捕获多个异常。这里有一部分的业务代码被放置在try 块里面,没有做到分离普通的业务代码和可能出现异常的代码,有碍迅速定位异常的位置。捕获多个异常的原理是如果try 块出现异常,运行时环境会将异常与catch 块中的异常类型逐一比较,最后抛出相应的异常类型。抛出多个异常的方法在程序不是特别复杂的情况下可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Properties p = Properties();
try {
p.load(new FileInputStream("sql_config.txt");
//业务代码没有分离
String drv = p.getProperty("drv");
String url = p.getProperty("url");
String usr = p.getProperty("usr");
String pwd = p.getProperty("pwd");
Class.forName(drv);
Connection conn = DriverManager.getConnection(url, usr, pwd);
//捕获多个异常
} catch(Exception e) {
//不处理异常
}

上面的代码有两个问题,一是没有对要捕获的异常类型进行明确的分类检查,这样会难以定位异常。第二点是没有对异常进行任何处理,如果发生任何异常并不进行任何处理,会极度影响系统调试,也会为系统安全和代码健壮性埋下隐患。在代码调试的阶段可以使用printStackTrace()通过打印异常栈来debug。但是当程序完成后,要对相应的异常进行具体处理。

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
public void login(String username, String password) {
//use isValidInput function to validate user input
String loginQuery = "select * from Users where username=? and password=?";
try {
Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(loginQuery);
ResultSet rs = pstmt.executeQuery();
pstmt.setString(1, username);
pstmt.setString(2, password);
//找到相应用户
if(rs.next()) {
//转到用户界面
}
} catch (SQLException e) {
//具体异常处理:通过弹出提示窗给用户
JOptionPane.showMessageDialog(new JFrame()
, "incorrect username/password"
, "login error"
, JOptionPane.ERROR_MESSAGE);
} finally {
pstmt.close();
conn.close();
}
}

上面的代码为捕获的异常进行了具体的处理,通过弹出错误窗口的方式提醒用户操作失败。