Java核心技术-异常、断言和日志
程序发生错误时至少做到以下几点:
*向用户通告错误
*保存所有的工作结果
*允许用户以妥善的形式退出程序
Java使用一种称为异常处理的错误捕获机制处理异常。
本章第一部分介绍Java的异常,第二部分介绍如何使用断言来有选择地启用检测,第三部分将讨论标准的Java日志框架。
1.1 处理错误
假设在一个Java程序运行期间出现了一个错误(文件包含错误信息、网络连接出现问题、无效的数组下标、试图使用一个没有被赋值的对象引用),程序应该:
*返回到一种安全的状态,并能够让用户执行一些其他命令
*允许用户保存所有操作的结果,并以妥善的方式终止程序
通常检测错误条件的代码离那些能够让数据恢复到安全状态或者能够保存用户的操作结果并正常退出的代码很远。异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。
1.2 异常分类
在Java中,异常对象都是派生于Throwable类的一个实例。
所有的异常都是由Throwable继承而来,但在下一层分解为两个分支:Error和Exception
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。
Exception又分解为两个分支:
1.由程序错误导致的异常属于RuntimeException:
*错误的类型转换
*数组访问越界
*访问null指针
2.而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
*试图在文件尾部豁免读取数据
*试图打开一个不存在的文件
*试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在
”如果出现RuntimeException就一定是你的问题“是一条相当有道理的规则。
应该通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException异常;
应该通过在使用变量之前检测是否为null来杜绝NullPointerException异常发生
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受查异常,所有其他的异常称为受查异常。
编译器将核查是否为所有的受查异常提供了异常处理器。
1.2 声明受查异常
如果遇到了无法处理的情况,那么Java的方法可以抛出一个异常。
方法应该在其首部声明所有可能抛出的异常(throws XXException)
在遇到下面4中情况时应该抛出异常:
1.调用一个抛出受查异常的方法,如FileInputStream构造器
2.程序运行过程中发现错误,并且利用throw语句抛出一个受查异常
3.程序出现错误,抛出一个非受查异常
4.Java虚拟机和运行时库出现的内部错误
对于前两种情况,则必须告诉调用这个方法的程序员有可能抛出异常。
后两种情况则不需要在方法首部声明
总结:一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制,要么就应该避免发生。
如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用,如果超类中没有抛出任何异常,子类也不能抛出任何异常。
如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常或这个类的任意一个子类的异常。
1.3 如何抛出异常
1.找到一个合适的异常类
2.创建这个类的一个对象
3.将对象抛出
1.4 创建异常类
在程序中,可能会遇到任何标准异常类都没有能够充分地描述清楚的问题。在这种情况下,就需要创建自己的异常类。
我们需要做的只是定义一个派生于Exception类或者Exception子类的类,习惯上,定义的类应该包含两个构造器,一个是默认构造器,另一个是带有详细描述信息的构造器。
class FileFormatException extends IOException { public FileFormatException(){} public FileFormatException(String gripe) { super(gripe); } }
2 捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行。
通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递。
不允许在子类的throws说明符中出现超过超类方法所列出的异常类范围。
2.1 捕获异常
Java中使用try/catch语句捕获异常
2.2 捕获多个异常
2.3 再次抛出异常与异常链
在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。
try { access the database } catch(SQLException e) { Throwable se=new ServletException("database error"); se.initCause(e); throw se; }
当捕获到异常时,就可以使用下面这条语句重新得到原始异常:
Throwable e=se.getCause();
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
奇淫技巧:
如果在一个方法中发生了一个受查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个受查异常,并将它包装成一个运行时异常。
2.4 finally子句
当代码抛出一个异常时,就会终止方法中剩余代码的处理,因此可能会产生资源回收问题。
Java中有一种解决方案,就是finally子句,不管是否有异常被捕获,finally子句中的代码都被执行。
try语句可以只有finally子句,而没有catch子句。
强烈建议解耦合try/catch和try/finally语句块。这样可以提高代码的清晰度(不能解决try和close同时抛出异常的情况):
InputStream in=...; try { try { code that might throw exception } finally { in.close(); } } catch(IOException e) { show error message }
内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告出现的错误。这种设计方式不仅清楚,而且还将会报告finally子句中出现的错误。
当finally子句中包含return语句时,将会出现一种意想不到的结果:
public static int f(int n) { try { int r=n*n; return r; } finally { if(n==2) return 0; } }
在方法返回前,finally子句的内容将被执行。如果finally子句中也有一个return语句,这个值将会覆盖原始的返回值。即以上代码调用f(2)的返回值是0。
2.5 带资源的try语句
带资源的try语句(try-with-resources)的最简形式为(假设资源属于一个实现了AutoCloseable接口的类):
try(Resource res=...) { work with res }
try块退出时,会自动调用res.close()。
如果try块抛出一个异常,而且close方法也抛出一个异常,带资源的try语句可以很好地处理这种情况——try中的异常被重新抛出,而close方法抛出的异常会“被抑制”。
2.6 分析堆栈轨迹元素
堆栈轨迹是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。
使用getStackTrace方法,它会得到StackTraceElement对象的一个数组,可以在你的程序中分析这个对象数组:
Throwable t=new Throwable(); StackTraceElement[] frames=t.getStackTrace(); for(StackTraceElement frame:frames) analyze frame
3 使用异常机制的技巧
使用异常机制的几个技巧:
1.异常处理不能代替简单的测试
捕获异常所花费的时间大大超过执行简单的测试,所以基本规则是——只在异常情况下使用异常机制。
2.不要过分地细化异常
将正常处理与错误处理分开
3.利用异常层次结构
将一种异常转换成另一种更加合适的异常时不要犹豫
4.不要压制异常
5.在检测错误时,“苛刻”要比放任更好
例如,当栈空时,Stack.pop是返回一个null,还是抛出一个异常,我们认为后者更好。
6.不要羞于传递异常
传递异常比捕获异常更好
4 使用断言
在一个具有自我保护能力的程序中,断言很常用。
4.1 断言的概念
断言机制允许在测试期间向代码插入一些检查语句。当代码发布时,这些插入的检测语句将会被自动地移走。
Java语言引入了关键字assert。有两种形式:
1.assert 条件;
2.assert 条件:表达式;
两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。
4.2 启用和禁用断言
默认情况下,断言被禁用。
可以在程序中用-enableassertions或-ea(开启默认包中的所有类断言)选项启用:
java -enableassertions MyApp
用-disableassertions或-da禁用断言
4.3 使用断言完成参数检查
在Java中,给出了3种处理系统错误的机制:
*抛出一个异常
*日志
*使用断言
什么时候使用断言:
*断言失败是致命的、不可恢复的错误
*断言检查只用于开发和测试阶段
断言只应该用于在测试阶段确定程序内部的错误位置。
4.4 为文档假设使用断言
断言是一种测试和调试阶段所使用的战术性工具;而日志记录是一种在程序的整个生命周期都可以使用的策略性工具。
5 记录日志
记录日志API用来帮助观察程序运行的操作过程:
*可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易
*可以很简单地禁止日志记录的输出,因此,这些日志代码留在程序中的开销很小
*日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等
*日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器指定的标准丢弃那些无用的记录项
*日志记录可以采用不同的方式格式化,例如,纯文本或XML
*应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字,例如,com.mycompany.myapp
*在默认情况下,日志系统的配置由配置文件提供。如果需要的话,应用程序可以替换这个配置
5.1 基本日志
“虚拟日志”
要生成简单的日志记录,可以使用全局日志记录器并调用其info方法:
Logger.getGlobal().info("File->Open menu item selected");
在适当的地方调用:
Logger.getGlobal().setLevel(Level.OFF);
将会取消所有日志
5.2 高级日志
“企业级日志”
在一个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义日志记录器
可以调用getLogger方法创建或获取记录器:
private static final Logger myLogger=Logger.getLogger("com.mycompany.myapp");
未被任何变量引用的日志记录器可能会被垃圾回收,为了防止这种情况发生,要用一个静态变量存储日志记录器的一个引用
通常,有以下7个日志记录器级别:
*SEVERE
*WARNING
*INFO
*CONFIG
*FINE
*FINER
*FINEST
默认情况下,只记录前三个级别
logger.setLevel(Level.FINE);
现在,FINE和更高级别的记录都可以记录下来
Level.ALL开启所有级别的记录,Level.OFF关闭所有级别的记录
对于所有的级别有下面几种记录方法:
logger.warning(message);
logger.fine(message);
logger.log(Level.FINE,message);
默认的日志记录将显示包含日志调用的类名和方法名,如果虚拟机对执行过程进行了优化,可以使用logp方法获得调用类和方法的确切位置:
void logp(Level l,String className,String methodName,String message)
跟踪执行流的方法(将生成FINER级别和以字符串ENTRY和RETURN开始的日志记录):
void entering(String className,String methodName,Object[] params)
void exiting(String className,String methodName,Object result)
记录日志的常见用途是记录那些不可预料的异常:
void throwing(String className,String methodName,Throwable t)——FINER级别的记录和一条以THROW开始的信息
void log(Level l,String message,Throwable t)
5.3 修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于:
jre/lib/logging.properties
要想用另一个配置文件:
1.将java.util.logging.config.file特性设置为配置文件的存放位置并使用命令java -Djava.util.logging.config.file=configFile MainClass
2.在main中调用System.setProperty("java.util.logging.config.file",file),也会调用LogManager.readConfiguration()来重新初始化日志管理器。
可以通过com.mycompany.myapp.level=FINE来指定自己的日志记录级别
可以通过java.util.logging.ConsoleHandler.level=FINE设置处理器级别
5.4 本地化
我们可能希望将日志消息本地化,以便让全球的用户都可以阅读它。
本地化的应用程序包含资源包,例如,某个资源包可能将字符串“readingFile”映射成英文“Reading file”.
每个资源包都有一个名字(如com.mycompany.logmessages)
在请求日志记录器时,可以指定一个资源包:
Logger logger=Logger.getLogger(loggerName,"com.mycompany.logmessages")
然后,为日志消息指定资源包的关键字,而不是实际的日志消息字符串:
logger.info("readingFile")
消息应该包含占位符{0}、{1},如:
Reading file {0}
然后调用下面的一个方法向占位符传递具体的值:
logger.log(Level.INFO,"readingFile",fileName);
5.5 处理器
在默认情况下,日志记录器将记录发送到ConsoleHandler中,并由它输出到System.err流中。特别是,日志记录器还会将记录发送到父处理器中。
与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。
日志管理器配置文件设置的默认控制台处理器的日志记录级别为:
java.util.logging.ConsoleHandler.level=INFO
要想记录FINE级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别。或者可以绕过配置文件,安装自己的处理器:
Logger logger=Logger.getLogger("com.mycompany.myapp"); logger.setLevel(Level.FINE); logger.setUseParentHandlers(false); Handler handler=new ConsoleHandler(); handler.setLevel(Level.FINE); logger.addHandler(handler);
在默认情况下,日志记录器将记录发送到自己的处理器和父处理器,所以设置useParentHandlers属性为false就不会两次看到这些记录。
要想将日志记录发送到其他地方,就要添加其他的处理器。日志API提供了两个很有用的处理器,一个是FileHandler,另一个是SockerHandler。
SockerHandler将记录发送到特定的主机和端口。FileHandler可以收集文件中的记录。
可以通过设置日志管理器配置文件中的不同参数来修改文件处理器的默认行为。
5.6 过滤器
在默认情况下,过滤器根据日志记录的级别进行过滤。
可以通过实现Filter接口并定义下列方法来自定义过滤器:
boolean isLoggable(LogRecord record)
要想将一个过滤器安装到一个日志记录器或处理器中,只需要调用setFilter方法。
注意,同一时刻最多只能有一个过滤器。
5.7 格式化器
ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。但是也可以自定义格式,这里需要扩展Formatter类并覆盖下面方法:
String format(LogRecord record)
最后,调用setFormatter方法将格式化器安装到处理器中。
5.8 日志记录说明
日志记录常用操作:
1.为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字(com.mycompany.myprog)
调用下列方法得到日志记录器:
private static final Logger logger=Logger.getLogger("com.mycompany.myprog");
2.默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台,可以通过修改默认的配置文件或者安装一个新的默认配置
3.级别为INFO、WARNING和SEVERE的消息都将显示在控制台上,因此,最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录设置为FINE级别。
6 调试技巧
1.可以打印或记录任意变量的值:
System.out.println("x="+x)或Logger.getGlobal().info("x="+x)
2.在每一个类中放置一个main方法,这样就可以对每一个类进行单元测试。
3.JUnit是一个非常常见的单元测试框架,学习并使用它
4.日志代理是一个子类的对象,它可以截获方法调用,并进行日志记录,然后调用超类中的方法。
可以以匿名子类实例的形式创建一个代理对象:
Random generator=new Random() { public double nextDouble() { double result=super.nextDouble(); Logger.getGlobal().info("nextDouble:"+result); return result; } }
5.利用Throwable类提供的printStackTrace方法,可以从任何一个异常对象中获得堆栈情况。
不一定要通过捕获异常来生成堆栈轨迹。只要在代码的任意位置插入下面这条语句就可以获得堆栈轨迹:
Thread.dumpStack();
6.一般来说,堆栈轨迹显示在System.err上,也可以利用printStackTrace(PrintWriter s)方法将它发送到一个文件中。
7.将非捕获异常记录到一个文件中:
Thread.setDefaultUncaughtExceptionHanler()
8.要想查看类的加载过程,可以使用-verbose标志启动Java虚拟机
9.-Xlint选项告诉编译器对一些普遍容易出现的代码问题进行检查
10.使用jconsole的图形工具
11.使用jmap查看堆的转储
12。使用-Xprof标志运行虚拟机可以运行一个剖析器跟踪代码中经常被调用的方法。输出结果中还会显示哪些方法是由即时编译器编译的。