前言

JAVA 提供了异常机制,使得程序有更好的容错性和健壮性。

# 异常概述

如果没有异常机制,为了尽可能地保证程序地正常运行,应该这么做:

if (JVM工作异常) {

}else if (网络不通) {

}else if (前置条件不满足) {

}else if (用户输入非法) {

}else {
	// 业务实现代码
}
1
2
3
4
5
6
7
8
9
10
11

这样的做法有两个问题:

  • 无法穷举所有的异常情况,总会有“漏网之鱼”,程序的健壮性差
  • 错误处理代码和业务实现代码混杂在一起,程序的可读性、可维护性差 通过 JAVA 的异常机制可以很好地解决异常。

# 异常类


Throwable:所有错误和异常的父类

  • Error:错误,一般是与虚拟机相关的问题,无法被处理
  • Exception:异常,表示程序自身的问题,应该被处理
    • Runtime 异常:运行时异常,程序运行过程中出现的问题 无需显式处理
    • Checked 异常:编译时异常,编译时出现的问题 必须显式处理,可以通过 try...catchthrows 处理

# Runtime 异常

可以通过修改代码解决的异常。

提示

数组越界、访问 null 对象等

# Checked 异常

无法通过修改代码(不包含错误处理代码,仅仅指业务实现代码)而解决的异常。

# Try...Catch

# 语法

try 
{
	···
} 
catch (异常类 变量名) 
{
	···处理异常···
}
1
2
3
4
5
6
7
8

# 处理机制

  • 如果在执行时出现了异常,系统会自动生成一个异常对象,该异常对象被抛出(提交给 JRE)
  • 当 JRE 收到异常对象时,会首先寻找能够处理该异常对象的 catch 块
    • 如果找到合适的 catch 块,就会将异常对象交给 catch 块处理,这个过程被称为抛出异常
    • 如果找不到合适的 catch 块,则程序终止
  • 进入 catch 块后,方法中的剩余代码都将不会被执行,并且其它 catch 块也不会被执行

# 排布规则

try 
{
	···
}
catch (RuntimeException e)
{
	System.out.println("运行时异常");
}
catch (Exception e)
{
	System.out.println("未知异常");
}
1
2
3
4
5
6
7
8
9
10
11
12

出现错误时,JAVA 总是从上到下检索 catch 块,如果将子异常类写在父异常类之后,则子异常类永远不会被执行。
因此,应当遵循这样的规则:

  • 将子异常类放置在前,父异常类放置在后
  • 将 Exception 放置在最后用于捕捉“漏网之鱼”

# 多异常捕获

JAVA 7 之后,一个 catch 可以用于捕获多种类型的异常,需要注意以下两点:

  • 多种异常类型之间用 | 隔开
  • 异常变量会被隐形 final 修饰,无法再被重新赋值
catch (异常类|异常类|异常类 e)
{
	···
}
1
2
3
4

# 异常信息的访问

catch (Exception e)
{
	System.out.println(e.getMessage());
	e.printStackTrace();
	System.out.println(e.getStackTrace());
	System.out.println(e);
	System.out.println(e.toString);
}
1
2
3
4
5
6
7
8

# finally

收尾工作
在程序中往往需要进行"收尾工作",例如:关闭数据库连接、关闭网络连接、关闭文件等。
根据异常类的处理机制,如果在 try 中放置"收尾动作",假设在“收尾工作”之前出现异常,程序会跳至对应的 catch 块,"收尾工作无法被执行";如果在 try 和 catch 中都放置"收尾动作",这的确能够实现"收尾"的要求,但代码大量重复,不是一个好的处理方式。
finally
JAVA 提供了 finally ,专门用于"收尾"。

try 
{
	···
} 
catch (异常类 变量名) 
{
	···处理异常···
}
catch (异常类 变量名) 
{
	···处理异常···
}
catch (异常类 变量名) 
{
	···处理异常···
}
finally
{
	···收尾···
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

无论 try 中是否出现异常,无论哪个 catch 块被执行,无论 try 和 catch 中是否执行了 return,finally 总会被执行
注意

  • finally 应该放置在所有 catch 的后面
  • 如果在 try、catch 中调用了退出虚拟机方法,则程序将会直接退出,finally 不会被执行
  • 如果在 finally 中使用了 returnthrow,将导致 try 和 catch 中的 returnthrow 失效

# throws

某些异常并不能在当前方法中处理,而是应该由上级方法进行处理,因此通过 throws 将异常抛回上级。
在上级中,可以对异常进行处理,或将异常继续向上抛出。如果 main 方法中也未处理异常并将异常向上抛出,则异常将交由 JVM 处理,JVM 会打印信息并终止程序运行。

# 语法

返回值 方法名() throws 异常类, 异常类 {
	···
}
1
2
3

在方法名之后填入 throws 异常类, 异常类,将指定类型的异常抛至上级。

# 方法重写时对 throws 的限制

子类方法抛出的异常类型应该是父类方法抛出的异常类型或其子类,
子类方法抛出的异常类型应该与父类方法抛出的异常类型相等或更少

# throw

在实际开发中,业务的"异常"往往不同于系统的"异常",如果程序中的数据、执行和既定的业务需求不符,便可以视为一种异常。这种异常显然无法被系统发现,因此需要程序员进行异常的抛出。

# 语法

throw 异常实例;
1

# 示例

throw new Exception("错误信息");
1

# 自定义异常类

可以自定义异常类,并且

  • 如果希望自定义 Checked 异常类,需要继承 Exception 类
  • 如果希望自定义 Runtime 异常类,需要继承 RuntimeException 类 通常情况下,提供两个构造器:
  • 无参构造器
  • 带一个字符串参数的构造器,字符串将作为描述信息
    示例:
public class 自定义异常类名 extends 异常类 {
	public 自定义异常类名(){
	
	}
	
    public 自定义异常类名(String msg){
		super(msg)
	}
}
1
2
3
4
5
6
7
8
9

# 嵌套使用

try...catch、throws、throw 可以嵌套使用,例如:

  • 对异常进行部分处理后再抛出,由外层继续处理
  • 获取异常信息后,抛出一个新的异常,在新的异常中对异常信息进行部分隐藏

# 注意事项

  • 不要过度使用异常
    • 对于代码能够解决的错误,应该通过修改代码的方式解决
    • 对于外部的、不能预知的错误,才通过异常解决
  • 异常机制的性能较差,能通过代码解决的问题应该尽量通过代码解决
  • 不要使用过于庞大的 try 块,应该拆分、分别捕获错误并处理
  • 不要直接用 catch(Exception e) 捕获所有错误,应该使用对应的异常类进行拦截
  • 不要忽略错误