处理错误
异常分类
异常对象都是派生于Throwable类的一个实例
下一层分解为两个分支,Error和Exception
Error类层次结构描述了运行时系统内部错误和资源耗尽错误,不应该抛出这种类型的对象
Exception类层次结构又分解为两个分支:IOException、RuntimeException
IOException类层次结构描述了I/O错误导致的异常,包括:
- 视图超越文件末尾继续读取数据
- 试图打开一个不存在的文件
- 视图根据给定的字符串查找Class对象,而这个字符串表示的类不存在
RuntimeException类层次结构描述了由编程错误导致的异常,包括:
- 错误的强制类型转换
- 数组访问越界
- 访问null指针
派生于Error类和RuntimeException类的所有异常称为非检查型异常
派生于IOException类的所有异常称为检查型异常
声明检查型异常
使用throws关键字声明检查型异常,异常之间用逗号隔开
public Image loadImage(String s) throws FileNotFoundException, EOFException {...}
声明一个异常,表示出错后会抛出一个异常对象,这个对象属于这个异常,或这个异常的子类
一个方法必须声明所有可能出现的检查型异常
子类方法中声明的异常类型不能比超类方法中声明的异常更通用
如果超类方法没有抛出任何检查型异常,则子类方法也不能抛出任何检查型异常
如何抛出异常
使用throw关键字抛出异常对象
throw new EOFException();
抛出异常对象后,系统会搜索处理这个异常对象的异常处理器,如果没有处理器捕获这个异常,当前执行的线程就会终止
一旦抛出了异常,这个方法就不会返回到调用者
可以直接捕获异常,而不需要抛出
创建异常类
创建自己的异常类,处理任何标准异常类都无法描述清楚的问题
这个类需要派生于Exception类的某个子类
通常包含两个构造器,一个无参数构造器,和一个包含详细描述信息的构造器
捕获异常
捕获异常
使用try/catch语句捕获一个异常
try
{
code
}
catch(ExceptionType e)
{
handler
}
如果try语句块中的任何代码抛出catch子句指定的一个异常,则程序跳过try语句的其余代码,并开始执行catch子句中的处理器代码
如果try语句块中的代码没有抛出异常,则跳过catch子句
如果try语句块中的任何代码抛出catch子句中没有声明的一个异常,则方法立即退出
通常捕获直到如何处理的异常,而抛出不知道怎么处理的异常
如果编写一个方法覆盖超类的方法,而这个超类的方法没有抛出异常,就必须捕获每一个检查型异常
捕获多个异常
在try语句块中可以捕获多个异常类型
每一个异常类型使用一个单独的catch子句
可以合并catch子句,但合并的catch子句中的异常类型之间不存在子类关系
try
{
code
}
catch(FileNotFoundException|UnknowHostException e) //合并catch子句
{
handler
}
catch(EOFException e)
{
handler
}
捕获多个异常时,异常变量隐含为final
合并后更简洁,更高效,生成的字节码只有一个公共的代码块
调用 getMessage方法得到详细的错误信息
调用 getClass( ) . getName( ) 得到异常的实际类型
再次抛出异常与异常链
可在catch子句中使用throw语句抛出异常,可用于修改异常的类型
使用logger.log方法记录一个异常
调用initCause方法把原始异常设置为新异常的原因
catch(EOFException original)
{
ver e = new ServletException;
e.initCause(original);
logger.log(level,message,e);
throw e;
}
调用getCause方法获取原始异常
Throwable original=e.getCause();
使用这种包装技术在子系统中抛出高层异常,而不会丢失原始异常的细节
finally子句
不管是否有异常被捕获,finally子句中的代码都会执行
抛出异常时,会终止线程并退出方法,必须考虑清理这个方法占有的本地资源
一种方法是捕获所有异常,并在catch子句完成资源的清理,再抛出异常。繁琐,且需要在正常的和异常的代码中分别清理资源
另一种方法是使用finally子句确保最后都会清理资源
var in = new FileInputStream;
try
{...}
catch(...)
{...}
finally
{
in.close();
}
try语句可以只有finally子句而没有catch子句
可以嵌套try语句,使内层确保清理资源,外层用于处理异常,而且还能报告finally子句中的异常
var in = new FileInputStream();
try
{
try
{...}
finally
{
in.close();
}
}
catch(...)
{...}
return语句执行后,会执行finally子句,而如果finally子句也有return语句,将覆盖原来的返回值
尽量不要把改变控制流的语句放在finally子句中
try-with-Resources语句
即带资源的try语句
try
(var in = new Scanner (System.in);
... ; ... ; ... ;
var out = new PrintWriter() )
{
...
}
当try块退出或异常时,自动调用close方法
java9中,可以在try首部中提供事实最终变量
如果try块抛出异常后,close方法也抛出一个异常,try块的异常将重新抛出,close方法抛出的异常被抑制,这些异常将被捕获,并由addSuppressed方法增加到try块的异常,使用getSuppressed方法得到这些增加的异常的数组
分析堆栈轨迹元素
堆栈轨迹是程序执行过程中某个特定点上所挂起的方法调用的一个列表
可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息
可以StackWalker类,生成StackWalker.StackFrame实例流,其中每个实例分别描述一个栈帧
StackWalker walker = StackWalker.getInstance();
可以调用迭代器访问
walker.forEach(frame->analyze... frame)
或使用懒方式处理
walker.walk(stream->process... stream)
使用异常的技巧
异常不能代替简单的测试,只在异常情况下使用异常
不要过分细化异常,有必要将整个任务包在一个try语句中,当一个操作出现问题时就可以取消整个任务
充分利用异常层次结构,不要只抛出RuntimeException异常,只捕获Throwable异常
可以将一个异常转换为另一种更合适的异常
不要为逻辑错误抛出异常
不要压制异常
在检测错误时,要苛刻
最好传递异常而不是自己捕获
使用断言
断言的概念
断言允许在测试期间向代码中插入一些检查,而在生成代码中会自动删除
使用assert关键字使用断言
assert condition;
assert condition:expression;
计算条件如果为false,则抛出AssertionError异常
表达式将传入AssertionError对象的构造器,并转换为一个消息字符,但并不储存具体的表达式
启用和禁用断言
默认情况下,断言是禁用的
使用-enableassertions或-ea命令启用运行程序的断言,或在某个类或包中启用断言
java -enableassertions MyApp
java -ea:MyClass -ea:com.mycompany.mylib MyApp
使用-disableassertions或-da命令禁用运行程序的断言,或在某个类或包中禁用断言
java -da:MyClass -da:com.mycompany.mylib MyApp
对于没有类加载器的系统类,使用-enablesystemassertions或-esa启用断言
也可以通过ClassLoader类中的方法控制类加载器的状态
使用setDefaultAssertionStatus、setClassAssertionStatus、setPackageAssertionStatus方法控制类加载器的状态
使用clearAssertionStatus删除所有显示的断言状态设置
不必重新编译程序来启用或禁止断言,启用和禁止断言是类加载器的功能
禁用断言时,类加载器会去除断言代码,不会降低程序运行的速度
使用断言完成参数检查
断言失败是致命的、不可恢复的错误
断言检查只是在开发和测试阶段打开
只用于在测试阶段确定程序内部错误的位置
将断言作为异常语句的前置条件,有时会抛出断言错误,有时会抛出语句的异常,取决于加载器的配置
日志
可以取消全部日志记录,或仅仅取消某个级别以下的日志,而且可以很容易的再次打开
可以简单的禁止日志记录,将这些代码留在程序中开销很小
日志记录可以被定位到不同的处理器
日志记录器和处理器可以对记录进行过滤,过滤器可以根据实现器指定的标准丢弃那些无用的过滤项
日志记录可以采用不同的方式格式化
应用程序可以使用多个日志记录器,他们使用与包名类似的层次结构的名字
日志系统的配置有配置文件控制
基本日志
要生成简单的日志记录,可以使用去全局日志记录器,并调用它的info方法
Logger.getGlobal().info("File->Open menu item selected");
调用全局日志记录器的setLevel方法并传递Level.OFF参数以取消所有日志
Logger.getGlobal().setLevel(Level.OFF);
高级日志
使用getLogger方法创建或获取指定日志记录器名的日志记录器
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
未被任何变量引用的日志记录器可能被垃圾回收,因此使用静态常量存储日志记录器的引用
日志记录器具有更强的层次结构,层次之间可以共享部分属性,并存在继承关系
日志级别:SEVERE、WARING、INFO、CONFIG、FINE、FINER、FINEST
默认情况下只记录前三个级别,可以使用更低级别记录用于诊断的调试信息
默认的日志处理器会抑制低级别的消息,需要修改日志处理器的配置
使用setLevel方法设置一个级别,以记录该级别及其上更高级别的日志
logger.setLevel(Level.FINE);
使用Level.ALL参数开启所有级别的记录,使用Level.OFF参数关闭所有级别的记录
所有级别有单独的日志记录方法:severe、warning、info、config、fine、finer、finest方法
logger.fine(message);
使用log方法记录日志并制定级别
logger.log(Level.FINE,message);
默认的日志记录将显示根据调用堆栈得出的包含日志调用的类名和方法名,但如果虚拟机做了某些优化,则信息将不准确
使用logp方法得到准确的调用类和方法的确切位置
logger.logp(Level.FINE,"com.mycompany.mylib.Reader","read",message);
使用entering方法和exiting方法跟踪执行流,并生成FINER级别且以ENTRY、RETURN开头的日志记录
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;
}
调用throwing方法或log方法在日志记录中包含异常的描述
if(...)
{
var e=new IOException("...");
logger.throwing("com.mycompany.mylib.Reader","read",e);
thorw e;
}
try
{...}
catch(IOException e)
{
Logger.getLogger("com.mycompany.myapp").log(Level.WARING,"...",e)
}
throwing调用可以记录一条FINER级别的日志记录和一条以THROW开始的消息
修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各个属性
在配置文件中编辑日志记录器的.level属性修改默认的日志级别,编辑处理器的.level属性以在控制台显示某级别的消息
com.mycompany.myapp.level=FINE
java.util.logging.ConsoleHandler.level=FINE
日志管理器在虚拟机启动时初始化
日志文件默认位于conf/logging.properties
使用-D选项将java.util.logging.config.file属性设置为指定位置的配置文件
java -D java.util.logging.config.file = ...
或在程序中使用静态System.setProperty方法设置为指定位置的配置文件,
使用LogManager.getLogManager()得到日志管理器,并调用readConfiguration()或updateConfiguration(mapper)重新初始化
System.setProperty("java.util.logging.config.file",file);
LogManager.getLogManager().updateConfiguration(mapper);
映射器mapper是一个Function<String,BiFunction<String,String,String>>函数
使用映射器来解析新老配置中所有键的值,将现有配置中的键映射到替换函数,替换函数接收与键关联的新老值
本地化
本地化的应用程序包含资源包中的本地特定信息,包括一组对应各个本地化环境的映射,对应每个环境提供一个纯文本文件
可以将这些文件与类文件放在一起,以便ResourceBundle类找到他们
请求一个日志记录器时可以指定一个资源包,然后为日志消息指定资源包中的键
Logger logger= Logger.getLogger("com.mycompany.myapp","com.mycompany.logmessages");
logger.info("readingFile");
可以在消息中使用占位符:{0},{1}等,然后在日志记录方法中向占位符传递具体的值,可以避免字符串连接操作
处理器
默认情况,日志记录器将记录发送到控制台处理器ConsoleHandler,由它输出到System.err流
通常日志记录器将记录发送到自己的处理器和父日志处理器
最终的祖先处理器名为"",ConsoleHandler是它的子类
日志记录的日志级别必须同时高于日志记录器和日志处理器
SocketHandler处理器将记录发送到指定的主机和端口
FileHandler处理器将记录收集到文件中
可以绕过日志管理器的配置文件,调用addHandler方法安装自己的处理器
Logger logger= Logger.getLogger(...); logger.setLevel(...); logger.setUseParentHandlers(...);
var handler=new FileHandler(); handler.setLevel(...); ...; ...;
logger.addHandler(handler);
默认情况,记录会格式化为XML
通过使用日志记录文件模式变量自定义日志文件名
还可以通过扩展Handler类或StreamHandler类自定义处理器
过滤器
默认情况,根据日志的级别进行过滤
定义一个过滤器,需要实现Filter接口,并定义isLoggable方法
调用setFilter方法将过滤器安装到日志记录器或处理器中
同一时刻最多有一个过滤器
格式化器
通过扩展Formatter类,并覆盖format方法定义格式化器
调用formatMessage方法将记录中的消息部分格式化,替换参数并应用本地化处理
调用setFormatter方法将格式化器安装到日志处理器中
调试技巧
可以打印或记录变量的值
可以在每个类中放置一个单独的main方法,以提供单元测试桩,独立地测试类
然后建立一些对象,调用所有方法,对各个文件分别启用虚拟机来进行测试
可以使用单元测试框架,如JUnit
可以使用日志代理,它是一个子类的对象,会截获方法调用,记录日志,然后调用超类中的方法
可以调用Throwable对象的printStackTrace方法,获得堆栈轨迹
可以将程序错误记入一个文件,在shell中使用命令java MyProgran 1>errors.txt 2>&1
可以使用静态Thread.setDefaultUncaughtExceptionHandler方法改变未捕获异常的处理器,将出现错误时的消息记录到一个文件
可以使用虚拟机的-varbose标志观察类的加载过程
可以使用编译器的-Xlint选项找出常见的代码问题
可以在虚拟机中安装代理来跟踪内存消耗、线程使用、类加载情况
可以使用java任务控制器,可以关联到正在运行的虚拟机,收集诊断和性能分析数据