很早之前就学习过C++
和Python
的异常处理,最近在Java
学习中遇到了异常处理的知识点,所以将三门较为通用的语言的异常处理机制作出总结和比较。
蒋炎岩老师在讲解调试理论时介绍过三个计算机术语的含义与差异,在此进行回顾:
错误(Error):一般是在意料中可能出现问题的地方出现了问题,导致实际情况偏离了期望。比如Linux
中常见的Error : No such file or directory
;
故障(Fault):程序中出现非故意的、不可预料的行为。fault通常是由error导致的。比如Segmentation Fault
(段错误);
失败(Failure):一个系统或者组件不能完成它被要求的功能。failure通常是由fault导致的。比如Java中的Build failed
。
一言以蔽之就是,人为的error
导致程序出现不可预料的fault
,最终使得整个程序系统failed
。而异常处理的目的,是在出现人为error
后防止fault
的发生,进而避免failure
。
我在学习C
的时候就发现它并没有类似C++
的异常处理机制,但并不代表无法进行错误处理,在C标准库error.h
中就定义了很多错误码($error\;\;code$)来辅助错误处理。当然我们也可以自己设置,我的matrix-C
中的客户程序就采用了这种方式:
enum error_type {
EVENT_BAD_EXPR, // 输入表达式不规范
EVENT_DEVIDE_ZERO, // 计算式除以0
EVENT_NO_EXPR, // 无输入
EVENT_MATCH_FAIL, // 正则表达式匹配错误
EVENT_OPERATOR_NOT_FOUND, // 输入无法识别的运算符
EVENT_NOT_ASSIGN_SYNTAX, // 无法识别的命令
EVENT_UNDEFIND, // 变量未定义
EVENT_SHAPE_DIFF, // 矩阵加减时形状不同
EVENT_MUL_SHAPE_DIFF, // 矩阵乘法中矩阵尺寸不合规范
EVENT_INV_SHAPE_DIFF, // 矩阵除法时矩阵尺寸不和规范
EVENT_INV_DET_ZERO, // 对一个行列式为0的矩阵求逆
EVENT_POW_PARAM_FALSE, // 矩阵幂的第二个操作符类型错误
};
大致流程如下,我们会在执行指令的时候遇到错误,此时就会修改error_code
,注册异常
error_code = -1 // no error
while true {
enter command
excute command while assigning error_code
handle error code
}
缺点?在C语言里,使用返回值等方式来处理错误,实际上很容易变成Bug的温床,因为没有强制性的检查,很容易被开发者疏忽。另外,可读性非常差,处理错误的代码和正常代码交织在一起,陷入「错误地狱」。
C++异常处理涉及三个关键字:try
、catch
和throw
:
throw
:意思是“抛出”异常;
catch
:在可能会出现error
的地方,用于“捕获”异常;
try
:try块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个catch
块。
程序实例,下面是一个实现整型除法的函数:
int division(int a, int b) {
if (b == 0) {
// 这里可以是任意的表达式
// 表达式的结果的类型决定了抛出的异常的类型
throw "Division by zero exception";
}
return a / b;
}
int main() {
division(1, 0);
}
Linux
下会输出:
terminate called after throwing an instance of 'char const*'
[1] 52704 abort (core dumped) ./test
我们throw
了一个字符串字面量,所以是char const*
的实例,我们当然可以改为其他类型,此时终端输出也会有差异。
catch
常常和try
一起使用,用于捕获异常。我们可以指定要捕捉的异常类型:
try {
TODO();
} catch (ExceptionName1 e1) {
HandleException1
} catch (ExceptionName1 e2) {
HandleException2
} ...
在上面的代码中,如果出现了类型为ExceptionName1
的异常,就会跳到第一个catch
块中进行处理,以此类推。如果想让catch
块能够处理try
块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号…:
try {
TODO();
} catch (...) {
HandleAllException
}
我们试着将三个关键字综合起来使用:
#include <iostream>
using namespace std;
double division(double a, double b) {
if (b == 0) {
throw "Division by zero exception";
}
return a / b;
}
int main() {
double a, b, c;
while (cin >> a >> b) {
try {
c = division(a, b);
cout << c << endl;
} catch (const char* msg) {
cerr << msg << endl;
}
}
}
输入一对a
和b
,输出其相除结果,如果除数为0则输出异常信息。由于我们抛出了一个类型为const char*
的异常,因此,当捕获该异常时,我们必须在catch
块中使用const char*
。
C++提供了一系列标准的异常,定义在<exception>
中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
在上面的程序中,我们实现了一个简单的异常处理,但这种字符串型的异常并不标准,我们可以看看vscode
所提供的异常处理代码补全:
try {
/* code */
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
e.what()
方法返回对应标准异常的提示信息。
我们可以通过继承和重载exception
类来定义新的异常:
#include <exception>
#include <iostream>
struct ooops : std::exception {
const char* what() const noexcept { return "Ooops!"; }
};
int main() {
try {
throw ooops();
} catch (const std::exception& ex) {
std::cerr << ex.what() << '\n';
}
return 0;
}
我们将ooops
作为exception
类的子类,然后对what
的方法进行重写,以输出自定义信息,也就是异常发生原因。
无抛出保证:你也许注意到了
noexcept
关键字,它被用来规定成员函数永远不会抛出异常,这也这也适用于C ++标准库中的所有派生类,在C++98中,写法是:cosnt char* what() const throw();
。
除此之外,断言是更简单更粗暴的一种异常处理,你可以在这里的Assert
获取加强版的断言。
在Pytohn
中,我们可以用try/except
语句来捕获异常:
try:
doSomething
except:
handleException
和C++中的try/catch
语法相似:
try:
x = int(input("Input a number: "))
except ValueError:
print("Not a number input!")
一个except
子句是可以处理多个异常的:
...
except (RuntimeError, TypeError, NameError):
pass
为了应对人为忽略的情况,我们会在最后一个子句收纳所有的异常:
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("OS error: {0}".format(err))
except ValueError:
print("Could not convert data to an integer.")
except:
'''打印未预料到的异常,然后把异常揪出来'''
print("Unexpected error:", sys.exc_info()[0])
raise
try/except
还有一个可选的else
子句,如果使用这个子句,那么必须放在所有的except
语句之后,它将在try
语句未发生任何异常是执行:
try:
doSomething
except Error1:
HandleError1
except Error2:
HandleError2
...
else:
print("No exception")
doMoreThings
使用
else
子句比把所有的语句都放在try
子句里面要好,这样可以避免一些意想不到,而except
又无法捕获的异常。
这是最完整的异常处理语句:
try:
doSomething
except Error:
HandleError
else:
'''没有异常时会执行的代码'''
doWithoutException
finally:
'''不管有没有异常都要执行的代码'''
doNoMatterException
我们发现,python
中,try
和C++中的try
等价,except
和C++中的catch
等价。实际上python
中也有和C++中throw
近似的关键字:raise
。
我们用raise
语句抛出一个指定异常:
raise [Exception [, args [, traceback]]]
例如:
x = 10
if x > 5:
raise Exception('x 不能大于 5。x 的值为: {}'.format(x))
这时就会触发异常:
Traceback (most recent call last):
File "test.py", line 3, in <module>
raise Exception('x 不能大于 5。x 的值为: {}'.format(x))
Exception: x 不能大于 5。x 的值为: 10
当我们已经捕获一个异常后,如果不想处理它,我们也可以在except
语句下,仅用raise
就可以抛出该异常:
>>> try:
raise NameError('HiThere')
except NameError:
print('An exception flew by!')
raise
An exception flew by!
Traceback (most recent call last):
File "<stdin>", line 2, in ?
NameError: HiThere
我们通过创建Exception
类的子类,重写方法来拥有自己的异常:
class MyError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
# 此时我们触发一个异常:
try:
raise MyError(2 * 2)
except MyError as e:
print('My exception occurred, value:', e.value)
执行后会输出:
My exception occurred, value: 4
当创建一个模块有可能抛出多种不同的异常时,一种通常的做法是为这个包建立一个基础异常类,然后基于这个基础类为不同的错误情况创建不同的子类。
我们甚至可以忽略except
子句:
try:
doSomething
finally:
cleanBehavior
如果一个异常在try
子句里(或者在except
和else
子句里)被抛出,而又没有任何的except
把它截住,那么这个异常会在finally
子句执行后被抛出。
Python
断言的语法很简单:
assert expression
等价于:
if not expression:
raise AssertionError
你可以像Java那样在断言中加入信息:
assert 1==2, '1 不等于 2'
和Python
类似,所有异常类都是从java.lang.Exception
类继承的子类。相关类的继承关系如下:
如上图所示,异常类有两个主要的子类:IOException
和RuntimeException
类
Java提供了两类主要的异常: runtime exception
和checked exception
,也就是运行时异常和检查性异常:
对于运行期异常,我们可以不需要处理运行时异常,当出现这样的异常时,总是由JVM接管。比如:我们从来没有人去处理过NullPointerException异常,它就是运行时异常,并且这种异常还是最常见的异常之一。
检查性异常,也就是我们经常遇到的IO异常,以及SQL异常都是这种异常。对于这种异常,JAVA编译器强制要求我们必需对出现的这些异常进行catch。所以,面对这种异常不管我们是否愿意,只能自己去写一大堆catch块去处理可能的异常。
使用try
和catch
可以捕获异常:
try {
// 程序代码
} catch (ExceptionName e1) {
// Catch块
}
你会发现这个写法几乎和C++的异常处理一模一样,在此不做赘述。
实例:
import java.io.*;
public class ExceptTest {
public static void main(String[] args) {
try {
int a[] = new int[2];
System.out.println("Access element three :" + a[3]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Exception thrown :" + e);
}
System.out.println("Out of the block");
}
}
上述代码编译运行输出如下:
Exception thrown :java.lang.ArrayIndexOutOfBoundsException: 3
Out of the block
类似与C++,Java也支持多重捕获块:
try {
// 程序代码
} catch (异常类型1 异常的变量名1) {
// 程序代码
} catch(异常类型2 异常的变量名2) {
// 程序代码
} catch(异常类型3 异常的变量名3) {
// 程序代码
}
实例:
try {
file = new FileInputStream(fileName);
x = (byte) file.read();
} catch(FileNotFoundException f) { // Not valid!
f.printStackTrace();
return -1;
} catch(IOException i) {
i.printStackTrace();
return -1;
}
如果一个方法没有捕获到一个检查性异常,那么该方法必须使用throws
关键字来声明。throws
关键字放在方法签名的尾部:
public void function(double amount) throws RemoteException,
InsufficientFundsExecption
{
// Method implementtation
if (something) {
throw new RemoteException();
}
}
在这里throws
后是函数中可能会出现的问题,标识以便于这样的处理:
try {
function();
} catch (RemoteException e1) {
handle exception 1
} catch (InsufficientFundsExecption e2) {
handle exception 2
}
也可以使用throw
关键字抛出一个异常,无论它是新实例化的还是刚捕获到的,这里就和C++中用法一模一样,不做赘述。
和Python
一样,我们会有一个finally
语句来创建在try
代码块后面执行的代码块(无论是否发生异常总会被执行)。在 finally 代码块中,可以运行清理类型等收尾善后性质的语句:
try {
// 程序代码
} catch(异常类型1 异常的变量名1) {
// 程序代码
} catch(异常类型2 异常的变量名2) {
// 程序代码
} finally {
// 程序代码
}
catch
不能独立于try
存在;
在try/catch
后面添加finally
块并非强制性要求的;
try
代码后不能既没catch
块也没finally
块;
try,
catch,finally
块之间不能添加任何代码。
Java也可以自定义异常,但有以下几点注意:
所有异常都必须是Throwable
的子类;
如果希望写一个检查性异常类,则需要继承Exception
类;
如果你想写一个运行时异常类,那么需要继承RuntimeException
类。
一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。
BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生:
public class BaseException extends RuntimeException {
}
其他业务类型的异常就可以从BaseException
派生:
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
...
自定义的BaseException
应该提供多个构造方法:
public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}
上述构造方法实际上都是原样照抄RuntimeException
。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。
同样,你也可以用assert
作为程序断言:
assert 1==2;
assert 1==2 : "1不等于2";