以《Java核心技术 卷1》第10版为主,结合自身实践进行截图及细节描述
异常、断言和日志
一个良好的程序应该包含错误处理机制,处理不良数据和问题代码。错误类型有:
- 用户输入错误;键盘输入错误,数据语法(邮箱,身份证号,URL)不正确
- 设备错误;
- 物理限制;磁盘满了
- 代码错误。
错误处理机制应有以下功能:
- 向用户通告错误;
- 保存所有的工作结果;
- 允许用户以妥善的方式退出程序。
Java中有三种错误处理机制:异常,断言和日志。
一、异常
1. 异常分类
在Java中,异常对象都是派生与Throwable类的一个实例。其异常层次结构如下:
- Error类:描述了Java运行时系统的内部错误和资源耗尽错误,应用程序员不应该抛出这种类型的对象。若出现了这样的内部错误,除了通告给用户,并尽力使程序安全地终止之外,就再也无能为力了。
- RuntimeException类:由程序错误导致的异常。如:错误的类型转换,数组访问越界,访问null指针等。
- 其它异常:程序本身没有问题,由于像I/O错误这类问题导致的异常属于其它异常(图中IOException只是其它异常中一种)。如:试图在文件尾部后面读取数据,试图打开一个不存在的文件等。
Java将派生于Error类或RuntimeException类的所有异常称为非受查(unchecked)异常,所有其它异常称为受查(checked)异常。编译器将核查是否为所有的受查异常提供了异常处理器。
2. 声明受查异常
关键字是throws,定义在方法名(包括构造器方法)之后
//FileInputStream 构造器
public FileInputStream(String name) throws FileNotFoundException
//一个方法可能抛出多个受查异常,应全部列出,逗号分隔
public Image loadImage(String s) throws FileNotFoundException,EOFException
//抛出多个受查异常的情况,可整合其共同超类
public Image loadImage(String s) throws Exception
在自己编写方法时,不必将所有可能抛出的异常都进行声明,需要记住遇到下面4种情况时会抛出异常:
- 调用一个抛出受查异常的方法,例如FileInputStream构造器
- 程序运行过程中发现错误,并且利用throw语句抛出一个受查异常
- 程序出现错误,如 某些运行时异常
- Java虚拟机和运行时库出现的内部错误
前两种情况必须声明受查异常。运行时异常完全在我们的控制之下,应将更多的精力花费在修正错误上,而不是说明这些错误发生的可能性上。不需要声明Java的内部错误,即从Error继承的错误,因为任何程序都具有抛出那些错误的潜力,而我们对此无能为力。
总之,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制(Error),要么应避免其发生(RuntimeException)。
其它规则:
- 子类中覆盖超类一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用(也就是说,子类方法中可以抛出更特定的异常,或者根本不抛出任何异常)
- 若类中方法声明抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个特定类的异常,或者这个类的任意一个子类的异常。
在方法块中抛出异常
- 找到一个合适的异常类
- 创建这个类的一个对象
- 将对象抛出
String readData(Scanner in) throws EOFException{
....
while(...){
if(!in.hasNext()){
throw new EOFException();
}
}
}
3. 创建异常类
定义一个派生于Exception的类或派生于Exception子类的类。派生类应包含两个构造器,一个是默认的构造器,一个是带有详细描述信息的构造器(超类Throwable
的toString方法将会打印出这些详细信息)。
//创建异常类
Class FileFormatException extends IOException{
private String message;
public FileFormatException(){}
public FileFormatException(String message){
super(message);
}
}
//可在其它方法中抛出创建的异常类
String readData() throws FileFormatException {
throw new FileFormatException();
}
4. 捕获异常
若某个异常发生时,没有在任何地方进行捕获,那程序就会终止执行,并在控制台打印出异常信息。捕获异常,必须设置try/catch语句块
// 1. 最简单的try语句块
try{
.....code....
}catch(Exception e){
.....
}
// 2. 捕获多个异常
try{
.....code....
}catch(FileNotFoundException e){
.....
}catch(UnknownHostException e){
......
}catch(IOException e){
.......
}
// 2. 捕获多个异常-合并catch子句
try{
.....code....
}catch(FileNotFoundException |UnknownHostException e){
System.out.println(e.getMessage());
.....
}
// 3. 再次抛出异常
try{
.....code....
}catch(SQLException e){
throw new ServletException("database error:"+ e.getMessage());
}
// 3. 再次抛出异常-包装技术
try{
.....code....
}catch(SQLException e){
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
// 4. finally子句
InputStream in = new FileInputStream(...);
try{
......
}catch(IOException e){
......
}finally{
in.close();
}
// 4. finally子句-无catch子句
try{
......
}finally{
in.close();
}
// 4. finally子句-解耦合
InputStream in=....;
try{
try{
}finally{
in.close();
}
}catch(IOException e){
.......
}
// 5. 带资源的try语句(最简形式)
try(Resourse res=....){
.....
}
// 5. 带资源的try语句,可指定多个资源
try(Scanner in = ....;PrintWriter out = .....){
......
}
规则如下:
- 若try语句块中的任何代码抛出一个在catch子句中说明的异常类,则程序将跳过try语句块的其余代码,将执行catch子句中的处理代码;
- 若try语句块中的代码没有抛出任何异常,程序将跳过catch子句;
- 若try语句块中的任何代码抛出一个没有在catch子句中说明的异常类,则这个方法会立即退出。(出现此种情况,应修改代码)
- 针对每个异常类对象e;可e.getMessage(); 获取详细的错误信息,e.getClass().getName() 得到异常对象的实际类型
- 在合并catch子句中,异常类型彼此之间不存在子类关系时才使用此特性,而且此时异常变量e隐含为final变量
- 再次抛出异常,其目的是改变异常的类型,让用户抛出高级异常,而不会丢失原始异常的细节。其原因:有时对同一异常存在多种解释,对应有多种解决方式,现不想知道发生错误的细节原因,但希望明确是哪一块有问题。
- 再次抛出异常-包装技术。SQLException被称为原始异常,se.initCause(e); 将原始异常设置为新异常的“原因”,Throwable e = se.getCause(); 重新获取原始异常。可以将受查异常包装成一个运行时异常,来实现不抛出目的。
- finally子句主要解决资源回收问题。不管是否异常被捕获,finally子句中的代码都被执行。try语句块可以没有catch子句,只有finally子句
- finally子句-解耦合:内层try语句块是确保关闭输入流,外层语句块是确保报告出现的错误。
- 当finally子句中有return语句时,由于finally子句始终最后执行,将会覆盖try语句块中return子句,而且当finally子句有异常抛出时,异常信息也会被覆盖。带资源的try语句将会解决异常信息覆盖问题。
- 带资源的try语句,当try语句块正常退出时,或存在一个异常时,都会自动调用close方法。在此处close方法抛出的异常会“被抑制”,这些被抑制的异常信息会通过addSuppressed方法增加导原来的异常中,可调用getSuppressed方法得到异常信息。
- 带资源的try语句自身也可以有catch子句和一个finally子句,在关闭资源后执行,但不推荐使用(给try语句块附加太多内容了)。
通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的遗产继续进行传递(即声明受查异常)。有一个例外,就是子类覆盖父类的方法时,不能声明比父类throws更广泛的异常,此时更广泛的异常应捕获处理了。
5. 分析堆栈轨迹元素
堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。核心类两个:Throwable类,StackTraceElement类。
返回类型 | 方法名 | 参数类型 | 描述 |
---|---|---|---|
void | printStackTrace | 无 | 将此throwable和其追溯打印到标准错误流 |
void | printStackTrace | PrintStream s | 将此throwable和其追溯打印到指定的打印流 |
StackTraceElement[] | getStackTrace | 无 | 获得构造这个对象时调用堆栈的跟踪 |
返回类型 | 方法名 | 参数类型 | 描述 |
---|---|---|---|
String | getFileName | 无 | 返回这个元素运行时对应的源文件名,若该信息不存在,返回null |
String | getClassName | 无 | 返回这个元素运行时对应的类的完全限定名 |
int | getLineNumber | 无 | 返回这个元素运行时对应的源文件行数,若信息不存在,返回-1 |
String | getMethodName | 无 | 返回这个元素运行时对应的方法名。 |
boolean | isNativeMethod | 无 | 若这个元素运行时在一个本地方法中,则返回true |
String | toString | 无 | 若存在,返回一个包含类名、方法名、文件名和行数的格式化字符串 |
6. 使用异常机制的技巧
技巧 | 解释 |
---|---|
异常处理不能代替简单的测试 | 与执行简单的测试相比,捕获异常所花费的时间大大超过前者 |
不要过分地细化异常 | 将整个任务包装在一个try语句块中,不要一条语句一个try语句块 |
利用异常层次结构 | 应寻找更适当的子类或创建自己的异常类来捕获或抛出 |
不要压制异常 | 若认为异常非常重要,就应该对它们进行处理 |
在检测错误时,“苛刻”要比放任更好 | 如:当栈空时,调用弹出元素的方法,是抛出异常呢?还是返回null。建议抛出异常,总比之后抛出空指针异常要好 |
不要羞于传递异常 | 让高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜 |
二、断言
断言机制允许在测试期间向代码中插入一些检查语句,当代码发布时,这些插入的检测语句会被自动移走。关键字:assert
1. 断言使用
// 对条件进行检测,结果为false,抛出AssertionError异常。
assert 条件;
// 对条件进行检测,结果为false,抛出AssertionError异常,且表达式将传入AssertionError的构造器,并转换成一个消息字符串。
assert 条件:表达式;
//举例
assert x>=0;
assert x>=0:x;
说明:“表达式” 的唯一目的时产生一个消息字符串,AssertionError对象并不存储表达式的值,因此,不可能在以后得到它。
2. 启用和禁用断言
# 命令行中执行
# 默认情况下,断言被禁用.可-enableassertions 或 -ea 选项启用
java -enableassertionsMyApp
# 在某个类或整个包中的使用断言
java -ea:MyClass -ea:com.mycompany.mylib... MyApp
# 可-disableassertions 或 -da 选项禁用
java -disableassertions MyApp
#-ea和-da不能应用到没有类加载器的“系统类”上,要用-enableSystemassertions / -esa 开关启用断言
说明: 启用或禁用断言是类加载器的功能,因此在启用或禁用时不必重新编译程序,启用断言也不会降低程序运行的速度。
在程序中也可以控制类加载器的断言状态。
返回类型 | 方法名 | 参数类型 | 描述 |
---|---|---|---|
void | setDefaultAssertionStatus | boolean | 对通过类加载器加载的所有类来说,若没有显式地说明类或包地断言状态,就启用或禁用断言 |
void | setClassAssertionStatus | String,boolean | 对给定类和它的内部类,启用或禁用断言 |
void | setPackageAssertionStatus | String,boolean | 对于给定包及其子包中的所有类,启用或禁用断言 |
void | clearAssertionStatus() | 移去所有类和包的显式断言状态设置,并禁用所有通过这个类加载器加载的类的断言 |
3. 使用断言其它规定
- 断言失败是致命的、不可恢复的错误
- 断言检查只用于开发和测试阶段
三、记录日志
日志功能:
- 可取消全部日志记录或者仅仅取消某个级别的日志,打开和关闭之这个操作很容易;
- 可禁止日志记录的输出;
- 日志记录可定向到不同的处理器:控制台,文件等;
- 日志记录器和处理器可对记录进行过滤;
- 日志记录可采用不同的方式格式化:纯文本或XML;
- 应用程序可使用多个日志记录器,它们使用类似包名具有层次结构的名字
- 默认情况下,日志系统的配置由配置文件控制,但应用程序也可替换这个配置
1. 日志记录器
// 全局日志记录器
Logger.getClobal().info(".....");
//自定义日志记录器
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
//控制日志级别
myLogger.setLevel(Level.FINE);
myLogger.setLevel(Level.ALL); //开启所有级别的记录
myLogger.setLevel(Level.OFF); //关闭所有级别的记录
//指定消息级别
myLogger.warning("...");
myLogger.fine("..."); //其它级别也有对应的方法
myLogger.log(Level.FINE,".....")
//默认的日志记录会显示日志调用的类名和方法名,但若虚拟机对执行过程进行了优化,就得不到准确的调用信息了。此时可调用logp方法获得类名和方法名
logp(Level level, String sourceClass, String sourceMethod, String msg);
// 跟踪方法开始和退出:entering与exiting
int read(String file,String pattern){
myLogger.entering("classname","methodName",new Object[]{file,pattern});
........
myLogger.exiting("classname","methodName",returnValue);
}
//记录异常,两种方式
if(...){ // throwing方式
myLogger.throwing("className","methodName",exception);
}
try{
......
}catch(Exception e){
myLogger.log(Level.WARNING,"message",e);
}
说明:
- 未被任何变量引用的日志记录器可能会被垃圾回收,故用静态变量存储日志记录器的引用
- 日志记录器具有层次结构,而且层次性比包强。子包与父包之间没有语义关系,但子日志记录器会继承父日志记录器的一些属性
- 日志记录级别有7个:SERVER、WARNING、INFO、CONFIG、FINE、FINER、FINEST。默认情况下,只记录前三个级别,也可设置其它级别,但需修改日志处理器的配置。
- entering,exiting方法将生成FINER级别和以字符串ENTRY和RETURN开始日志记录。因其是FINER级别,所以要进行日志处理器级别的配置,否则出不来。
- throwing 可以记录一条FINER级别的记录和一条以THROW开始的信息
返回类型 | 方法签名 | 描述 |
---|---|---|
static Logger | getGlobal() | 返回名为Logger.GLOBAL_LOGGER_NAME的全局记录器对象 |
static Logger | getLogger(String name) | 查找或创建一个命名子系统的记录器 |
void | info(String msg) | 记录INFO消息 |
void | setLevel(Level newLevel) | 设置日志级别,指定该记录器将记录哪些消息级别 |
void | warning(String msg) | 记录警告消息 |
void | fine(String msg) | 记录fine消息 |
void | log(Level level, String msg) | 记录消息,没有参数 |
void | logp(Level level, String sourceClass, String sourceMethod, String msg) | 记录消息,指定源类和方法,没有参数。 |
void | entering(String sourceClass, String sourceMethod) | 记录方法条目 |
void | entering(String sourceClass, String sourceMethod, Object param1) | 使用一个参数记录方法条目 |
void | entering(String sourceClass, String sourceMethod, Object[] params) | 使用参数数组记录方法条目 |
void | exiting(String sourceClass, String sourceMethod) | 记录方法返回 |
void | exiting(String sourceClass, String sourceMethod, Object result) | 使用结果对象记录方法返回 |
void | throwing(String sourceClass, String sourceMethod, Throwable thrown) | 日志抛出异常 |
void | log(Level level, String msg, Throwable thrown) | 使用相关联的Throwable信息记录消息 |
2. 修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性,默认情况下,配置文件存在于:jre/lib/logging.properties。
# 配置文件
java.util.logging.config.file =... #使用另一个配置文件
.level = INFO # 修改默认的日志记录级别
com.mycompany.myapp.level = FINE #可指定自己的日志记录级别
java.util.logging.ConsoleHandler.level = INFO #控制台处理器级别设置
说明:使用另一个配置文件也可在main方法中调用System.setProperty(“java.util.logging.config.file”,file)进行修改。日志属性文件由LogManager类处理。
3. 处理器
默认情况下,日志记录器将记录发送到ConsoleHandler中,并由它输出到Systerm.err流中,特别地,日志记录器还会将记录发送到父处理器中,而最终处理器(命名为“”)有一个ConsoleHandler。
与日志处理器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。
要想记录FINE级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别,但也可在程序中安装自己的处理器,如下:
// 修改配置文件
.level= INFO
java.util.logging.ConsoleHandler.level = INFO
//程序中安装处理器
Logger logger = Logger.getLogger("com.mycompany.myapp");
logger.setLevel(Level.FINE);
logger.setUseParentHandlers(false); //因记录会发送到自己的处理器和父处理器,为不看到两次记录,故设置为false
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
logger.addHanler(handler);
补充:除了ConsoleHandler,还有其它处理器:FileHandler(将日志保存到指定文件),SocketHandler(将记录发送到指定的主机和端口)
返回类型 | 方法签名 | 描述 |
---|---|---|
void | setUseParentHandlers(boolean useParentHandlers) | 指定此记录器是否应将其输出发送到其父记录器 |
void | addHandler(Handler handler) | 添加日志处理程序以接收日志消息 |
配置属性 | 描述 | 默认值 |
---|---|---|
java.util.logging.FileHandler.level | 处理器级别 | Level.ALL |
java.util.logging.FileHandler.append | 追加到一个已存在的文件尾部或打开新文件 | false |
java.util.logging.FileHandler.limit | 在打开另一文件之前允许写入一个文件的近似最大字节数 | 50000(0表示无限制) |
java.util.logging.FileHandler.pattern | 日志文件名的模式 | %h/java%u.log |
java.util.logging.FileHandler.count | 在循环序列中的日志记录数量 | 1(不循环) |
java.util.logging.FileHandler.filter | 使用的过滤器类 | 没有使用过滤器 |
java.util.logging.FileHandler.encoding | 使用的字符编码 | 平台的编码 |
java.util.logging.FileHandler.formatter | 记录格式器 | java.util.logging.XMLFormatter |
变量 | 描述 |
---|---|
%h | 系统属性user.home的值 |
%t | 系统临时目录 |
%u | 用于解决冲突的唯一编号 |
%g | 为循环日志记录生成的数值 |
%% | %字符 |
说明:
- 若多个应用程序使用同一个日志文件,就应该开启append标志。另外,应该在文件名模式中使用%u,以便每个应用程序创建日志的唯一副本。
- 推荐开启文件循环功能
自定义处理器
可通过扩展Handler类或StreamHandler类自定义处理器。
因处理器会缓存记录,且只有在缓存满时才将它们写入流中。因此,需覆盖publish方法,以便在处理器获得每个记录之后刷新缓冲区。扩展Handler类,还需重写publish、flush和close方法
class WindowHandler extends StreamHandler{
public WindowHandler(){
......
}
public void publish(LogRecord record){
super.publish(record);
flush();
}
}
4. 过滤器
默认情况下,过滤器根据日志记录的级别进行过滤。每个日志记录器和处理器都可以有一个可选的过滤器来完成附加的过滤。
可通过实现Filter接口并定义isLoggable方法来自定义过滤器。此方法返回true表示记录应包含在日志中。
可调用setFilter方法将过滤器安装到日志记录器或处理器中,同一时刻最多只能有一个过滤器。
5. 格式化器
ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。也可自定义格式:扩展Formatter类并重写format方法。最后调用setFormatter方法将格式化器安装到处理器中。
四、调试技巧
- 可用System.out.print(…) 或 日志 打印任意变量的值
- 在每个类中放置一个单独的main方法,进行单元测试。(推荐使用Junit)
- 日志代理,即截获方法调用,进行日志记录之后,再调用方法
Random generator = new Random(){
public double nextDouble(){
double result = super.nextDouble();
Logger.getGlobal.info("nextDouble: "+result);
return result;
}
};
// 此时当调用nextDouble方法时,就会产生一个日志消息。
- 利用Throwable类提供的printStackTrace方法,可以从任何一个异常对象中获得堆栈情况;也可在代码任何位置写入Thread.dumpStack() 获得堆栈轨迹。
try{
......
}catch(Throwable t){
t.printStackTrace();
throw t;
}
Thread.dumpStack();
- 一般来说,堆栈轨迹显示在System.err上,可利用printStackTrace(PrintWriter s)方法将其发送到文件中,或者一个字符串中。
StringWriter out = new StringWriter();
new Throwable.printStackTrace(new PrintWriter(out));
String description = out.toString();
- 捕获错误流
# 捕获错误流:System.err
java MyProgram 2 > errors.txt
# 同时捕获System.err和System.out
java MyProgram 1 > errors.txt 2>&1
- 观察类的加载过程,用 -verbose 标志启动Java虚拟机,有助于诊断由于类路径引发的问题
- -Xlint 选项告诉编译器对一些普遍容易出现的代码问题进行检查
javac -Xlint # 执行所有检查
javac -Xlint:all # 执行所有检查
javac -Xlint:deprecation # 与-deprecation一样,检查
javac -Xlint:fallthrough # 检查switch语句中是否缺少break语句
javac -Xlint:finally # 警告finally子句不能正常地执行
javac -Xlint:none # 不执行任何检查
javac -Xlint:path # 检查类路径和源代码路径上的所有目录是否存在
javac -Xlint:serial # 警告没有serialVersionUID的串行化类
javac -Xlint:unchecked # 对通用类型与原始类型之间的危险转换给予警告
- Java虚拟机增加了对Java应用程序进行监控和管理的支持。jconsole程序。