写在前面
Java的基本理念是"结构不佳的代码不能运行"。
发现错误的理想时机是在编译阶段, 也就是在你试图运行程序之前。然而, 编译期间并不能找出所有的错误, 余下的问题必须在运行期间解决。这就需要错误源能通过某种方式, 把适当的信息传递给某个接收者——该接收者将知道如何正确处理这个问题。
错误恢复在我们所编写的每一个程序中都是基本的要素, 但是在Java中它显得格外重要, 因为Java的主要目标之一就是创建供他人使用的程序构件。要想创建健壮的系统, 它的每一个构件都必须是健壮的。Java使用异常来提供一致的错误报告模型, 使得构件能够与客户端代码可靠地沟通问题。
异常处理是Java中唯一正式的错误报告机制, 并且通过编译器强制执行。
概念
"异常"这个词有"我对此感到意外"的意思。问题出现了, 你也许不清楚该如何处理, 但你的确知道不应该置之不理; 你要停下来, 看看是不是有别人或在别的地方, 能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题, 所以就把这个问题提交到一个更高级别的环境中, 在这里将做出正确的决定。
使用异常所带来的另一个明显的好处是, 它往往能够降低错误处理代码的复杂度, 如果不适用异常, 那么就必须检查特定的错误, 并在程序的许多地方去处理它。而如果使用异常, 那就不必在方法调用处进行检查, 因为异常机制将保证能够捕获这个错误。并且, 只需在一个地方处理错误, 即所谓的异常处理程序中。这种方式不仅节省代码, 而且把"描述在正常执行过程中做什么事"的代码和"出了问题怎么办"的代码相分离。总之, 与以前的错误处理方法相比, 异常使代码的阅读、编写和调试工作更加井井有条。
基本异常
普通问题是指, 在当前环境下能够得到足够的信息, 总能处理这个错误。而对于异常情形, 就不能继续下去了, 因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出, 并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。
当抛出异常后, 有几件事会随之发生。首先, 同Java中其他对象的创建一样, 将使用new在堆上创建异常对象。然后, 当前的执行路径被终止, 并且从当前环境中弹出对异常对象的引用。此时, 异常处理机制接管程序, 并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序, 它的任务是将程序从错误状态中恢复, 以使程序能要么换一种方式运行, 要么继续运行下去。
异常最重要的方面之一就是如果发生问题, 它们将不允许程序沿着其正常的路径继续走下去。
与使用Java中的其他对象一样, 我们总是new在堆上创建异常对象, 这也伴随着存储空间的分配和构造器的调用。所有标准异常类都有两个构造器: 一个是默认构造器; 另一个是接受字符串作为参数, 以便能够把相关信息放入异常对象的构造器:
throw new NullPointerException("t = null");
捕获异常
要明白异常是如何被捕获的, 必须首先理解监控区域(guarded region)的概念。它是一段可能产生异常的代码, 并且后面跟着处理这些异常的代码。
try块
如果在方法内部(或在方法内部调用的其他方法)抛出了异常, 这个方法将在抛出异常的过程中结束。要是不希望方法就此结束, 可以在方法内设置一个特殊的块来捕获异常。因为在这个块里"尝试"各种(可能产生异常的)方法调用, 所以称为try块。它是跟在try关键字之后的普通程序块:
try{
// Code that might generate exceptions
}
所以有了异常处理机制, 可以把所有动作都放在try块里, 然后只需在一个地方就可捕获所有异常, 这意味着代码将更容易编写和阅读, 因为完成任务的代码没有与错误检查的代码混在一起。
异常处理程序
抛出的异常必须在异常处理程序得到处理, 而且针对每个要捕获的异常, 得准备响应的处理程序。异常处理程序紧跟在try块之后, 以关键字catch表示:
try{
// Code that might generate exceptions
}catch(Type1 id1){
// Handle exceptions of Type1
}catch(Type2 id2){
// Handle exceptions of Type2
}catch(Type3 id3){
// Handle exceptions of Type3
}
异常处理程序必须紧跟在try块之后。当异常被抛出时, 异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入catch子句执行, 此时认为异常得到了处理。一旦catch子句结束, 则处理程序的查找过程结束。注意, 只有匹配的catch子句才能得到执行;这与switch语句不同, switch语句需要在每一个case后面跟一个break, 以避免执行后续的case子句。
try块内部, 不同的方法调用可能会产生类型相同的异常, 而你只需要提供一个针对此类型的异常处理程序
创建自定义异常
Java提供的异常体系不可能预见所有的希望加以报告的错误, 所以可以自己定义异常类来表示程序中可能会遇到的特定问题。
要自己定义异常类, 必须从已有的异常类型继承, 最好是选择意思相近的异常类继承(不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生默认构造器
也可以为异常类定义一个接受字符串参数的构造器
捕获所有异常
可以只写一个异常处理程序来捕获所有类型的异常。通过捕获异常类型的基类Exception, 就可以做到这一点(事实上还有其他的基类, 但Exception是同编程活动相关的基类):
catch(Exception e){
System.out.println("Caught an exception");
}
这将捕获所有异常, 所以最好把它放在处理程序列表的末尾, 以防它抢在其他处理程序之前先把异常捕获了
可以调用它从其基类Throwable继承的方法:
String getMessage()
String getLocalizedMessage()
用来获取详细信息, 或用本地语言表示的详细信息
void printStackTrace()
void printStackTrace(PrintStream)
void printStackTrace(java.io.PrintWriter)
Throwable fillInStackTrace()
此外, 也可以使用Throwable从其基类Object(也是所有类的基类)继承的方法, 对于异常来说, getClass()也许是一个很好用的方法, 它将返回一个表示此对象类型的对象。然后可以使用getName()方法查询这个Class对象包含包信息的名称, 或者使用只产生类名称的getSimpleName()方法。
应用
在Java语言中,异常的继承结构大致是:
如果调用的某个方法抛出了非RuntimeException,则必须在源代码中使用try...catch或throws语法,否则,源代码将报错!而RuntimeException不会受到这类语法的约束!
在项目中,如果需要通过抛出异常来表示某种“错误”,应该使用自定义的异常类型,否则,可能与框架或其它方法抛出的异常相同,在处理时,会模糊不清(不清楚异常到底是显式的抛出的,还是调用其它方法时由那些方法抛出的)!同时,为了避免抛出异常时有非常多复杂的语法约束,通常,自定义的异常都会是RuntimeException的子孙类异常。
另外,抛出异常时,应该对出现异常的原因进行描述,所以,在自定义异常类中,应该添加带String message参数的构造方法,且此构造方法需要调用父类的带String message参数的构造方法。
则在项目的根包下创建ex.ServiceException异常类,继承自RuntimeException,例如:
然后,在Service中,就抛出此类异常,并添加对于错误的描述文本,例如:
在Controller中,将调用Service中的方法,可以使用try..catch包裹这段代码,对异常进行捕获并处理,例如: