Github代码链接: https://github.com/deyou123/corejava.git
第七章 异常、断言和日志
由于程序的错误或一些外部环境的影响造成用户数据的丢失,为了避免:
- 向用户通告错误;
- 保存所有的工作结果;
- 允许用户以妥善的形式退出程序;
异常处理 :可能造成程序崩溃的错误输入。
7.1 处理错误
如果由于出现错误二十的某些操作没有完成,程序应该:
- 返回到一种安全状态,并能够让用户执行一些其他的命令;
- 允许用户保存所有操作的结果,并以妥善的方式终止程序。
这并不容易:检测错误条件的代码通常离那些能让数据恢复到安全状态,或者能够保存用户的操作结果,并正常的退出程序的代码很远。
异常处理的任务就是将控制权从错误产生的地方转移给能处理这种情况的错误处理器。
1、用户输入错误 (URL)
2、设备错误(打印机)
3、物理限制(磁盘满了)
4、代码错误
程序方法有可能无法正确执行。
7.1.1 异常分类
异常对象都是派生于Throwable类的一个实例。
-
Error:描述了Java运行时系统的内部错误和资源耗尽错误。
- 应用程序不应该抛出这种类型的对象。
- 如果出现了这样的内部错误,除了通告给用户,并尽力使程序安全的终止之外,在也无能为力了。
-
Exception:
- RuntimeException:由程序错误导致的异常
- 错误的类型转换
- 数组访问越界
- 访问null指针
- 其他异常:程序本身没有问题,由像IO错误这类问题导致的异常属于其他异常。
- 试图在文件尾部后面读取数据
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
- RuntimeException:由程序错误导致的异常
如果使RuntimeException异常,那么一定是你的问题。
处理不存在的文件;可能在检查他是否存在之前就已经被删除了;因此“是否存在”却决于环境,而不是只是取决于你的代码;
非受查异常: 派生于Error或RuntimeException类的所有异常。
受查异常: 所有的其他异常。
- 编译器将核查是否为所有的受查异常提供了异常处理器。
7.1.2声明受查异常
-
如果遇到无法处理的情况,那么java的方法可以抛出一个异常。
-
一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。
-
读取文件的代码可能读取的文件不存在,或内容为空;就会可能抛出IOException;
-
方法应该在其首部生命所有可能抛出的异常;从首部反映出这个方法可能抛出哪类受查异常。
-
在自己编写方法时,不必将所有可能抛出的异常都进行声明;
以下情况应该抛出异常:
1) 调用一个抛出受查异常的方法:FileInputStream构造器。
2)程序运行过程中发现错误,并且利用throw语句抛出一个受查异常
3)程序出现错误: a[-1]:Array’Index’OutofBoundsException
4)Java虚拟机和运行时库出现的内部错误。
- 1和2 必须告诉调用这个方法的程序员有可能抛出异常。
- 其他可能被他人使用的java方法,在方法首部生命这个方法可能抛出的异常。
- 如果有多个受查异常类型,用逗号隔开;
- 不需要声明java的内部错误,即从Error继承的错误;任何程序代码都具有抛出那些异常的潜能,并且我们对其没有任何控制能力。
- 同样,也不应该声明从RuntimeException继承的那些非受查异常;这些运行时错误完全在我们的控制之下;ex:数组下标,应该将更多的时间花费在修正程序中的错误上,而不是说明这些错误发生的可能性上。
- 总之,一个方法必须声明所有可能抛出的受查异常;而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException);
警告
- 如果在子类覆盖了一个超类的方法,子类方法中声明的受查异常不能比超类方法中生命的异常更通用:也就是说,子类方法中可以抛出更特定的异常(特殊化而不是通用化),或是根本不抛出任何一场。
- 如果超类没有抛出任何受查异常,子类也不能抛出任何受查异常。
7.1.3 如何抛出异常
EOFException(String msg):在输入过程中,遇到一个未预期的EOF后的信号;
对于一个已存在的异常类,将其抛出非常容易:
1、找到一个合适的异常类
2、创建这个类的一个对象
3、将对象抛出
一旦方法抛出了异常,这个方法就不可能返回到调用者;不必为返回的默认值或错误代码担忧。
7.1.4 创建异常类
- 需要定义一个派生于Exception或派生于Exception子类(RuntimeException等)的类。
- 应该包含两个构造器,一个默认构造器;一个带有描述详细信息的构造器;
7.2 捕获错误
7.2.1 捕获异常
- 如果某个异常发生时没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息;其中包括异常的类型和堆栈的内容。
- 对于图形界面程序,在捕获异常后,也会打印出堆栈的信息,但程序将返回到用户界面的处理循环中(调试GUI程序时,最好保证控制台窗口可见,并且没有被最小化)
捕获异常,必须设置try-catch语句块。
-
如果try语句块中任何代码抛出了一个在catich子句中说明的异常类:
- 程序将跳过try语句块的其余代码
- 程序将执行catich子句中处理器代码
-
如果try语句中的代码没有抛出异常,那么程序将跳过catich子句。
-
如果方法中的任何代码抛出了一个在catch子句中没有生命的异常类型,那么这个方法会立刻退出。
-
通常,最好选择什么也不做,而是将异常传递给调用者;这样方法可能抛出一个异常。
-
throws,如果调用了一个抛出受查异常的方法,就必须对他处理,或者继续传递。
-
通常,应该捕获那些知道如何处理的异常,而将那些不知道处理的异常继续进行传递。
-
如果像传递一个异常,就必修在方法的首部添加一个throws说明符,一边告知调用者这个方法可能抛出异常。
-
例外:如果编写一个没有抛出异常的, 覆盖父类的方法,那么这个方法就必须捕获方法代码中出现的每一个受查异常;不允许子类的throws说明符中出现超过父类方法所列出的异常类范围。
7.2.2 捕获多个异常
- 每个异常类型使用一个单独的catch子句。
- 假设缺少文件和未知主机异常的动作是一样的,就可以合并catch子句(并且异常类型彼此之间不存在子类关系):
catch(FileNotFoundException | UnknownHostException e){
//,,...
}
7.2.3 再次抛出异常与异常链
- catch子句中可以抛出一个异常,可以改变异常的类型。
- 将原始一场设置为新异常的原因
try{}
catch(SQLException e){
Throwable se = new ...
se.initCause(e);
throw e;
}
Throwable e = se.getCause();
- 包装技术:可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
- 如果不允许抛出受查异常,包装技术十分有用。
- 有时只想记录一个异常,再将他重新抛出,而不做任何改变。
try{}
catch(Exception e){
logger.log(level, message, e);
throw e;
}
7.2.4 finally子句
-
异常退出方法之前,回收资源问题;
-
不管是否有异常被捕获,finnaly子句的代码都会被执行
执行finally子句的情况:
1、代码没有抛出异常。 try语句块全部代码,finally代码, try-catch-finally之后的代码。
2、抛出一个在catch子句中捕获的异常。 try语句到发生异常的代码, 跳过剩余代码,执行相应的catch子句代码,finally代码。- 如果没有抛出异常,执行之后的代码。
- 抛出异常,就不执行后续代码。
3、抛出一个异常,但这个异常不是由catch子句捕获的。try语句到发生异常,跳过剩余代码,finally子句,并将异常抛给调用者。
-
try语句可以只有finally子句,而没有catch子句。
提示
- 建议解耦合 try-catch 和 try-finally语句块。
InputStream in = ...;
try{
try{
}
finally{in.close();}
}
catch(IOException e){
show error message
}
- 内层try确保关闭输入流;外层try报告出现的错误,包括finally子句中的错误。
警告
-
当finally子句包含return语句时,将会出现一种意想不到的结果。
-
finally子句的return语句,这个返回值将会覆盖原始的返回值。
-
有时候finally子句,清理资源的方法也有可能抛出异常;
-
这样原有的异常会丢失,转而抛出finally子句的异常。
7.2.5 带资源的try语句
带资源的try语句最简单形式为:
try(Resource res = …){ work with res}
try块退出时,会自动调用res.close()。
try(Scanner in = new Scanner(new FileInputStream("filename")), "UTF-8")
{
while(in.hasNext())sout(in.next());
}
如果带有多个资源,用;隔开。
无论是否有异常,都会抛出异常。
- 捕获异常,由 addSuppressed方法增加到原来的异常
- 可以调用getSuppressed方法,得到抛出的异常列表。
7.2.6 分析堆栈轨迹元素
堆栈轨迹:是一个方法调用过程的列表,包含了程序执行过程中方法调用的特定位置。
- 可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
- 更灵活的方法是使用getStackTrace方法,他会得到StackTraceElement对象的一个数组,可以在程序中分析这个对象数组;
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame: frames)
...
- StackTraceElement类含有能够获得文件名和当前执行的代码行号的方法,同时,还含有能够获得类名和方法名的方法。
- 静态Thread.getrAllStackTrace方法,可以产生所有线程的堆栈轨迹:
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for(Thread t : map.keySet())
{
StackTraceElement[] frames = map.get(t);
...
}
7.3 使用异常机制的技巧
1、异常处理不能代替简单的测试
与执行简单的测试相比,捕获异常所花费的时间大大超过了前者; 只在异常情况下使用异常机制。
2、不要过分的细化一场
将正常处理与错误处理分开。
3、利用一场层次结构
- 不要之抛出RuntimeException异常,应该寻找更加适当的子类或创建自己的异常类。
- 不要只捕获Throwable异常,否则会使得程序代码更难读、更难维护。
- 已经查异常本来就很庞大,不要为逻辑错误抛出这些异常。
- 将一种异常转换成另一种更加适合的异常时不要犹豫。
4、不要压制异常
- java中,往往强烈的倾向关闭异常。
5、在检测错误时,“苛刻”要比放任更好
当栈空时,Stack.pop时返回一个null还是抛出一个异常?
在出错的地方抛出一个EmptyStackException异常要比在后面抛出一个NullPointerException异常更好;
6、不要羞于传递异常
让高层次的方法通知用户放生了错误,或者放弃不成功的命令更加适宜。
5、6: 早抛出,晚捕获。
7.4 使用断言
7.4.1 断言的概念
- 断言机制允许在测试期间像代码中插入一些检查语句;
- 当代码发布时,这些插入的检查语句就会被自动的一走。
- assert关键字的两种形式:
-
- assert 条件;
-
- assert 条件:表达式;
-
- 如果结果为false,就抛出一个AssertionError异常;第二种形式中,表达式被传入AssertionError的构造器,并转换成一个消息字符串。
- “表达式”的唯一目的是产生一个消息字符串;
- ex:
assert x >= 0;
assert x>=0: x;将x的实际值传递给AssertionError对象,可以在后面显示出来。
7.4.2 启用和禁用断言
- 默认情况,断言被禁用;
- 启用: java -enableassertions/ -ea MyApp;
- 在启用或禁用断言时不必重新编译程序;者时类加载器的功能。
- 当断言被禁用时,类加载器将跳过断言的代码;不会降低程序运行速度。
- 也可以在某个包中使用断言:
java -ea:MyClass -ea:com.mycompany.mylib… MyApp
将开启MyClass类以及在com.mycompany.mylib包和他的子包中的所有类的断言。 - 选项-ea 将开启默认包中的所有类的断言。
- 禁用断言: -disableassertions / -da
7.4.3 使用断言完成参数检查
java的3中处理系统错误机制:
- 抛出一个异常
- 日志
- 使用断言;
什么时候选择使用断言:
- 断言失败是致命的、不可恢复的错误;
- 断言检查只用于开发和测试阶段;
因此,不应该使用断言向程序的其他部分通告发生了可恢复性的错误;或者,不应该作为程序向用户通告问题的手段。
断言之应该用于在测试阶段确定程序内部的错误位置。
例子。。。
7.4.4 为文档假设使用断言
7.5 记录日志
sout调试
记录日志API的优点:
- 可以很容易的取消全部日志记录,或者仅仅取消某个级别的日志;而且打开和关闭这个操作也很容易;
- 可以很简单的禁止日志记录的输出;留日志代码开销很小;
- 日志记录可以被定向到不同的处理器;用于在控制台中显示、存储在文件中等;
- 日志记录器和处理器都可以对记录进行过滤;过滤器可以根据锅炉实现器制定的标准丢弃那些无用的记录项;
- 日志记录可以采用不同的方式格式化;村文本或XML;
- 应用程序可以使用多个日志记录器;他们使用类似包名的层次结构;
- 在默认情况下,日志系统的配置由配置文件控制;应用程序可以替换这个配置。
7.5.1 基本日志
- 生成简单日志记录,使用全局日志记录器(global logger)并调试info方法:
Logger.getGlobal().info(“File->Open menu item selected”); - 在默认情况下,这条记录将会显示以下内容:
May 10,2013 …
INFO: File->Open menu item selected - 取消所有的日志:
Logger.getGlobal().setLevel(Level.OFF);
7.5.2 高级日志
- 调用getLogger方法创建或获取记录器:
private static final Logger myLogger = Logger.getLogger(“com.mycompany.myapp”); - 未被任何变量引用的日志记录器可能会被垃圾回收;需要用一个静态变量存储日志记录器的一个引用。
- 日志记录器名也具有层次结构:
1、SEVERE
2、WARNING
3、INFO
4、CONFIG
5、FINE
6、FINER
7、FINEST - 默认情况,只记录前三个级别;也可以设置其他级别
ex: logger.setLevel(Level.FINE); - 使用Level.ALL开启所有级别的记录,或者Level.OFF关闭所有级别的记录。
- ex:
logger.warning(msg);
logger.fine(msg);
logger.log(Level.FINE, msg);
- 默认的日志配置记录了INFO或更高级别的所有记录;
- 如果将记录级别设计为INFO或者耕地,则需要修改日志处理器的配置。
- 默认的日志记录将显示包含日志调用的类名和方法名;如果虚拟机对执行过程进行了优化,就得不到准确的调用信息;此时可以调用logp方法获得调用类和方法的确切位置:
void logp(Level l,String className,String msg);
一些跟踪执行流的方法:
void entering(String className,String methodName);
…
ex:
int read(String file,String pattern)
{
logger.engtering("com.mycompany.mylib.Reader","read",new Object[]{file,pattern});
...
logger.exiting("com.mycompany.mylib.Reader","read",count);
return count;
}
- 记录日志的常见用途是记录那些不可预料的异常:
void throwing(String className; String methodName, Throwable t);
void log(Level l,String msg,Throwable t);
ex:
if(...)
{
IOException exception = new IOException("...");
logger.throwing("com...class","func",exception);
throw exception;
}
try{}
catch(IOException e)
{
logger.getLogger("com.company.app").log(Level.WARNING,"Reading iamge",e);
}
7.6 调试技巧
1、打印或记录任意变量的值:
sout
Logger.getGlobal().info();
2、单元测试、main方法
3、JUnit测试
4、日志代理
。。。。
5、利用Throwable类提供的printStackTrace方法,不得堆栈情况。
6、利用printStaceTrace(PrintWriter s)方法将他发送到一个文件中
StringWriter out = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(out));
out.toString();
7、 将程序中的错误信息保存在一个文件中:
java MyProgram 1 > errors.txt 2 > &1
8、