对于异常情况,Java使用了一种称为异常处理(exception handing)的错误捕获机制
处理错误
在开发过程中,如果由于出现错误而使得某些操作没有完成,程序应该:
- 返回到一种安全状态,并能够让用户执行其他的命令。或者
- 允许用户保存所有的工作,并以妥善的方式终止程序。
在Java中,如果某个方法不能够采用正常的途径完成,可以通过另外一个途径退出。这种情况下,方法不返回任何值,而是抛出(throw)一个封装了错误信息的对象。
需要注意的是,这个方法会立刻退出,并不是返回正常值。也不会继续调用这个方法剩下的代码。取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器(exception handler)
异常分类
在Java中,异常对象都是继承自Throwable类。
Throwable有两个子类:分别是Error和Exception
-
Error:描述了Java运行时系统的内部错误和资源耗尽错误。这种错误除了通知用户,并尽力妥善的终止程序之外,几乎没有其他办法。
-
Exception:Exception是我们需要关注的重点。Exception又分解成两支
-
Runtime Exception:由变成错误导致的异常
派生于Runtiome Exception的异常包括:
- 错误的强制类型转换
- 数组访问越界
- 访问null指针
- …
-
other:其他异常
- 试图超越文件末尾继续读取数据
- 试图打开一个不存在的文件
- 试图查找一个不存在的Class对象
- …
-
Java语言中,规定派生于Error或者Runtiome Exception的异常称为非检查型异常(unchecked ),所有其他类型的异常称为检查型异常(checked)。
编译器会检查是否所有的检查型异常都提供了异常处理器。
声明检查型异常
定义一个方法,除了定义方法的形参、返回值和方法体外,还要告诉编译器这个方法可能发生什么错误。
我们需要再方法的首部指出这个方法可能抛出的异常,使用throws子句。
public void method1() throws NullPointerException, IOException, FileNotFoundException {
System.out.println("hello exception");
}
使用throws子句声明可能抛出的异常时,如果该方法可能抛出多种检查型异常,就在throws子句中列出所有的异常类,并用逗号隔开。
如果子类覆盖了父类的一个方法,子类方法中声明的检查型异常不能比父类方法中声明的异常更通用(子类方法可抛出更特定的异常或者不抛出异常)。如果父类方法并未抛出检查型异常,子类方法也不能抛出任何检查型异常。
如何抛出异常
抛出异常使用throw关键字。
抛出异常非常容易:
- 找到合适的异常类
- 创建这个异常类的对象
- 将对象抛出
public void method2() throws NullPointerException {
User user = new User(1,"test");
if(user == null){
// 创建异常对象并抛出
throw new NullPointerException("空指针");}
}
一旦方法抛出了异常,这个方法就不能返回到调用者。也就是说,不必操心建立一个默认的返回值或错误码。
创建异常类
当代码中遇到任何标准异常类都无法描述的问题时,我们可以创建自定义异常。
自定义异常类只需继承Exception类或者Exception的子类。并提供两个构造方法:无参构造器和一个带描述信息的有参构造器。
/**
* 自定义异常
*/
public class MyException extends Exception{
// 无参构造器
public MyException() {
}
// 带描述的构造器
public MyException(String message) {
super(message);
}
}
捕获异常
除了使用throw向上抛出异常,也可以将异常进行捕获。
捕获异常
如果发生了某个异常,但是没有再任何地方进行捕获,程序就会终止,并在控制台上输出异常的类型和堆栈轨迹等。
要对异常进行捕获,需要使用try/catch语句块:
try
{
// 可能发生异常的代码
} catch(/*异常对象*/)
{
// 处理方式
}
如果try语句块中的代码抛出了catch子句指定的异常类,那么:
- 程序将跳过try语句块的剩余代码
- 程序执行相对应的catch子句中的处理器代码
如果try语句中没有发生任何异常,那么程序将跳过所有catch子句。
如果try语句中抛出了catch子句没有声明的异常类,那么这个方法会立即退出。
关于抛出与捕获异常如何选择
一般经验是,捕获那些你知道如何处理的异常;而向上抛出你不知道如何处理的异常
捕获多个异常
一个try语句块中可以捕获多种异常,并对不同类型的异常有不同的处理
try
{
// 可能发生异常的代码
} catch(/*异常对象1*/)
{
// 处理方式1
}catch(/*异常对象2*/)
{
// 处理方式2
}catch(/*异常对象3*/)
{
// 处理方式3
}catch(/*异常对象4*/)
{
// 处理方式4
}
异常对象可能包含有关异常性质的信息。想要获得这个对象的更多信息,可以使用:
getMessage() // 获得详细信息
getClass().getName() // 获得异常对象的实际类型
再次抛出异常与异常链
我们可以再catch子句中抛出一个异常。通常,这么做是想改变异常的类型。
try
{
// 可能发生异常的代码
} catch(/*异常对象*/)
{
throw new OthreException(); // 向上抛出新的异常
}
finally子句
代码抛出异常时,就会停止当前方法的剩余代码并退出该方法。但是我们经常会需要即使抛出了异常,也需要完成某些代码,比如资源清理等。这里可以使用finally子句。
不管是否有异常被捕获,finally子句中的代码均会执行。
try
{
// 可能发生异常的代码
} catch(/*异常对象*/)
{
// 处理方式
} finally
{
// 关闭资源等必须执行的代码
}
这个程序可能再一下三种情况执行finally子句:
- 代码没有抛出异常
- 代码抛出了异常,并被catch捕获
- 代码抛出了异常,但是没有被任何catch子句捕获
总之,无论try语句块是否遇到异常,finally子句均会执行。
分析堆栈轨迹
堆栈轨迹(stack trace)是程序执行过程中某个特定点上所有挂起的方法调用的一个列表。
当Java程序因为一个未捕获异常而终止时,就会显示堆栈信息。
可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。
异常使用技巧
- 异常处理不能代替简单的测试
- 不要过分细化异常
- 充分利用异常的层次结构
- 不要压制异常
- 再检测错误时候,”苛刻“比放任更好
- 不要羞于传递异常
使用断言
建设中。。。
日志
我们都熟悉在有问题的代码中插入一些System.out.print代码来帮助观察程序的行为。当问题代码调试正常后,我们需要手动删除这些System.out.print代码;如果代码又出现问题,我们又需要再次添加进去。这种做法效率非常底下。
日志就是为了解决这个问题而设计的,日志在使用过程中有如下优点:
- 可以很容易的取消全部日志记录,或者取消某个级别一下的日志,而且可以很容易的再次打开。
- 可以很简单的禁止日志记录。因此,将这些日志代码留在程序中的开销很小。
- 日志记录可以被定向到不同的处理器。如在控制台显示、写至文件等。
- 日志记录和处理器都可以对日志进行过滤。过滤器可以根据过滤器实现器指定的标准丢弃那些无用的记录项。
- 日志记录可以采用不同的方式格式化。
- 应用程序可以使用多个日志记录器。他们使用的包名类似的有层次结构的名字。
- 日志系统的配置由配置文件控制
简单日志
生成最简单的日志,可以使用全局日志记录器。作用等于System.out.print,代码执行后在控制台显示
Logger.getGlobal().info("基本日志");
如果在合适的地方(如main方法最前面)调用:
Logger.getGlobal().setLevel(Level.OFF);
将会取消所有日志。
高级日志
一个专业的程序中,不会将所有的日志都记录到一个全局日志记录器中。而是使用具有层次性的日志记录器。
可以调用getLogger方法创建日志记录器:
private static final Logger logger = Logger.getLogger("priv.ihch17.logger.higherLogger");
未被任何变量引用的日志记录器可能会被垃圾回收。为了防止这种情况发生,需要使用静态变量存储日志记录器的引用。
通常,日志有以下7个级别,从高到低分别为:
- SERVER
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
默认情况下,只记录前三个级别。也可以通过以下设置,记录降低记录的级别:
logger.setLevel(Level.FINE)
也可以使用Level.All开启所有日志记录,或者使用Level.OFF关闭所有级别。
日志技巧
- 对于一个简单的应用,选择一个日志记录器。可以将日志记录器命名为主应用包一样的名字。如果有大量日志记录活动,可以将记录器增加static属性。
- 默认的日志配置会将级别高于等于INFO的所有消息记录到控制台上。用户可以根据实际需求修改这一配置。