11 异常、断言、日志和调试
异常处理(exception handing)
使用断言来启动检测
Java日志框架
调试技巧
11.1 处理错误
如果一个方法不能够采用正常的途径完成任务,就通过另外一个路径退出方法。
在这种情况下,方法不返回任何值,而是抛出一个封装了错误信息的对象。此外,调用这个方法的代码也将无法继续执行。
异常处理机制开始搜索能够处理这种异常情况的异常处理器(exception handler)。
异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。
异常对象是派生于Throwable类的一个实例。
可以创建自己的异常类。
异常的分类:
Throwable | Exception | RuntimeException |
|
ArrayIndexOutOfBoundsException | |||
NullPointerException | |||
IOException |
| ||
| |||
Error |
|
|
Error类层次描述的是Java运行时系统的内部错误和资源耗尽。
应用程序不应抛出此类错误。
除了通告给用户并尽力使程序安全地中止之外,再也无能为力。
此种情况很少出现。
派生于Error类或RuntimeException类的所有异常称为未检查(unchecked)异常;
所有其他的异常称为已检查(checked)异常。
编译器将检查是否为所有已检查异常提供了异常处理器。
声明已检查异常:
public FileInputStream(String name) throws FileNotFoundException
一个方法要么返回其返回值,要么抛出一个异常。
一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控(Error),要么应避免(RuntimeException)。
如果子类中覆盖了超类的一个方法,子类方法中声明的已检查异常不能比超类方法中声明的异常更通用。
如何抛出异常:
1、找到一个合适的异常类;
2、创建其类对象;
3、将对象抛出。
如:throw new EOFException();
throw new EOFException(String);
一旦方法抛出异常,这个方法就不能返回到调用者。
C++中可以抛出任何类型的值,Java中只能抛出Throwable子类的对象。
创建异常类:
定义一个派生于Exception的类,或者派生于Exception子类的类。
包含两个构造器:默认构造器;带有详细描述信息的构造器。
class FileFormatException extends IOException
{public FileFormatException(){}public FileFormatException(String message){super(massage);}
}
11.2 捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈信息。
捕获异常:
try
{code that might throw exception
}
catch(FileNotFoundException | UnknownHostException e) //捕获多个异常时,e隐含为final变量。
{emergency action for missing files and unknown hosts
}
catch(IOException e)
{emergency action for all other I/O problems
}
如果try语句块中的任何代码抛出了一个在catch子句中说明的异常类,则跳过try语句块的其余代码,执行catch子句中的处理器代码;
如果try语句块中抛出的异常没有在catch子句中说明,则立即退出这个方法;
如果try语句块没有抛出异常,则跳过catch子句。
通常,异常处理的最好选择是什么都不做,而是将异常传递给调用者。
如果调用了一个抛出已检查异常的方法,要么处理,要么传递。
通常,捕获那些知道如何处理的异常,传递那些不知怎样处理的异常。
在catch子句中还可以抛出一个异常,这样做目的是改变异常的类型。
如:
try
{access the database
}
catch (SQLException e)
{Throwable se = new ServletException(“database error”);se.initCause(e);throw se;
}
finally
{close resource;
}Throwable e = se.getCause();
//可以得到原始异常,使用这种包装技术,可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
如果在一个方法中发生了一个已检查异常,而方法不允许抛出它,那么包装技术可以将这个已检查异常包装成一个运行时异常。
强烈建议使用try/catch、try/finally子句。
finally子句一定会被执行。
带资源的try语句
try(Resource res = ...)
{work with res;
}
//try块退出时,自动调用res.close()方法。
//此时,try块抛出的异常被抛出,close方法抛出的异常被抑制。
带资源的try语句自身也可以有catch子句和一个finally子句。这些子句会在关闭资源后执行。
在实际中,一个try语句中加入这么多内容可能不是一个好主意。
堆栈跟踪(stack trace):
是一个方法调用过程的列表,包含了程序执行过程中方法调用的特定位置。
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)analyze frame;
//多线程堆栈跟踪
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Thread t : map.keySet())
{StachTraceElement[] frames = map.get(t);analyze frames
}
11.3 使用异常的技巧
1、异常处理不能代替简单的测试;
2、不要过分细化异常;
3、利用异常层次结构;
4、不要压制异常;对于不常发生的异常,catch语句块的内容可以先空着,待日后感觉需要处理时再填上。
5、不要羞于传递异常。
11.4 使用断言
断言机制允许在测试期间向代码中插入一些检查语句。
当代码发布时,这些插入的检测语句将会被自动移走。
assert 条件;
assert 条件:表达式;
这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。
在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。
表达式的唯一目的就是产生一个消息字符串。
assert x >= 0;
assert x >=0 : x;
assert x >=0 “x >= 0”;
默认情况下,断言被禁用。
在运行时,用-enableassertions 或 -ea 选项启用它。
java -enableassertions MyApp
在启用或禁用断言时不必重新编译程序。启用或禁用断言时类加载器(class loader)的功能。
当断言被禁用时,类加载器将跳过断言代码,因此不会降低程序运行的速度。
也可选用-disableassertions或-da禁用某个特定类或包的断言。
在Java中,给出了三种处理系统错误的机制:
异常;
日志;
断言。
断言是致命的,不可恢复的错误。只用于开发和测试阶段。
断言是一种测试和调试阶段所使用的战术性工具;
日志记录是一种在程序的整个生命都可以使用的策略性工具。
11.5 记录日志
优点:
·可以取消全部日志记录,或仅取消某个级别的日志,而且打开和关闭这个操作也很容易;
·可以很简单的禁止日志记录的输出,因此,这些日志代码留在程序中的开销很小;
·日志记录可以在控制台中显示,也可以在文件中存储;
·可以对日志记录进行过滤,只保留重要日志;
·可以采用不同格式:纯文本或XML;
·可使用多个日志记录;
·默认情况下,日志系统的配置由配置文件控制;如果需要的话,应用程序可以替换这个配置。
基本日志:
日志系统管理着一个名为Logger.global的默认日志记录器,可通过info方法记录日志信息
Logger.getGlobal().info(“File->Open menu item selected”); //记录日志
Logger.getGlobal().setLevel(Level.OFF); //取消所有日志
高级日志:
在一个应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是自定义日志记录器。
调用getLogger方法可以创建或检索记录器:
private static final Logger myLogger = Logger.getLogger(“com.mycompany.myapp”);
日志记录具有层次结构,上下层之间将共享某些属性,例如对com.mycompany日志记录器设置了日志级别,它的子记录器也会继承这个级别。
SERVER WARNING INFO CONFIG FINE FINER FINEST
默认记录前三个级别。
级别设置:
logger.setLevel(Level.FINE); //Level.ALL开启所有级别 Level.OFF关闭所有级别
记录方法:
logger.waring(message);
logger.fine(message);
logger.log(level.FINE, message);
默认的日志配置记录了INFO或更高级别的所有记录。
应该使用CONFIG、FINE、FINEST级别来记录那些有助于诊断,但对于程序员又没有太大意义的调试信息。
默认的日志记录将显示包含日志调用的类名和方法名,如同堆栈所显示的那样。
但是,如果虚拟机对执行过程进行了优化,就得不到准确的调用信息。
此时可用logp方法获得调用类和方法的确切位置:
void logp(Level l, String className, String methodName, String Message)
跟踪执行流的方法:
void entering(String className, String methodName)
void entering(String className, String methodName, Object param)
void entering(String className, String methodName, Object[] params)
void exiting(String className, String methodName)
void exiting(String className, String methodName, Object result)
如:
int read(String file, String pattern)
{logger.entering(“com.mycompany.mylib.Reader”,”read”,new Object[]{file, pattern};...logger.exiting(“com.mycompany.mylib.Reader”,”read”,count);return count;
}
这些调用将产生FINER级别和以字符串ENTRY和RETURN开始的日志记录。
日志记录的常见用途是记录那些不可预料的异常。
两种方法:
void throwing(String className, String methodName, Throwable t)
void log(Level l, String message, Throwable t)
典型用法:
if(...)
{IOException exception = new IOException(“...”);logger.throwing(“com.mucompany.mylib.Reader”,”read”,exception);throw exception;
}
//调用throwing可以记录一条FINER级别的记录和一条以THROW开始的信息。try
{...
}
catch(IOException)
{Logger.getLogger(“com.mycompany.myapp”).log(Level.WARNING, “Reading image”, e);
}
修改日志管理器配置文件jre/lib/logging.properties
日志管理器在VM启动过程中被初始化,在main之前执行。
.level=INFO //修改默认的日志记录级别
com.mycompany.myapp.level=FINE 指定自己的日志记录级别
本地化:
本地化的应用程序包含资源包(resource bundle)中的本地特定信息。
资源包由各个地区的映射集合组成。如某个资源包可以将redingFile映射成英文的Reading file或者德文的Achtung! Datei wired eingelesen。
一个程序可以包含多个资源包,一个用于菜单;其他用于日志消息。
每个资源包都有一个名字,如com.mycompany.logmessages。
要想将映射添加到一个资源包中,需要为每个地区创建一个文件。
英文消息映射位于com/mycompany/logmessages_en.properties文件中;
德文消息映射位于com/mycompany/logmessages_de.properties文件中。
可以将这些文件与应用程序的类文件放在一起,以便ResourceBundle类自动对它们进行定位。
这些文件都是纯文本文件,内容如下:
readigFile=Achtung! Datei wird eingelesen
renamingFile=Datei wird umbenannt
在请求日志记录器时,可以指定一个资源包:
Logger logger= Logger.getLogger(loggerName, “com.mycompany.logmessages”);
然后,为日志消息指定资源包的关键字,而不是实际的日志消息字符串:
logger.info(“readingFile”);
通常需要在本地化的消息中增加一些参数,因此,消息应该包括占位符{0}、{1}等。
例如,在日志消息中包含文件名,需要使用下列方式包含占位符:
readingFile=Reading file {0}
renamingFile=Change name file {0} to {1}
logger.log(Level.INFO, “readingFile”, fileName);
logger.log(Level.INFO, “renamingFile”, new Object[]{oldName, newName};
处理器:
默认情况下,日志记录器将记录发送到ConsoleHandler中,并由它输出到System.err流中。
日志记录器还会将记录发送到父处理中,最终处理器(命名为””)有一个ConsoleHandler。
处理器也有一个日志记录级别。
安装自己的处理器:
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);
其他处理器:
FileHandler 将记录发送到用户主目录的javan.log文件中,n是文件的唯一编号,默认XML格式。
SocketHandler 将记录发到特定的主机和端口。
过滤器:
默认情况下,过滤器根据日志记录的级别进行过滤。
每个日志记录器和处理器都可以有一个可选的过滤器来完成附加的过滤。
另外,可通过实现Filter接口的boolean isLoggable(LogRecord record)方法来自定义过滤器。
调用setFilter方法安装过滤器。
一个时刻最多只能有一个过滤器。
日志记录说明:日志记录器最好与主应用程序包名相同;