04.异常处理
04.异常处理
4.1 程序需要考虑的错误类型
- 用户输入错误
- 设备错误,一些设备可能不存在或出错
- 物理限制,存储空间可能不够了
- 代码错误
4.2 java异常分类
- java中异常都是由
Throwable继承而来,但是在下一层分解为Error和Exception,由Error和RuntimeException派生而来的称为非检查型错误,否则称为检查型错误- Error是系统内部错误和资源耗尽错误,应用程序不应该抛出该类型错误
- Exception分解为IO类和Runtime错误,编程错误的异常属于Runtime,文件流的错误属于IO,如果出现Runtime错误一定是你自己的问题
4.3 异常抛出
- 使用
throw new 异常名()抛出对应类型的异常,new 异常名()可以生成对应的异常类 - 函数抛出,如果整个函数有可能抛出异常,就可以使用
函数名(参数) throw 异常名{ 函数内容}声明这个方法可能发生异常
4.4 自定义异常类
- 可以从已有的异常类派生出新的异常类,异常初始化时,可以引入一个字符串,描述该异常的详细信息,在
Throwable类开始就可以使用toString方法或getMessage方法输出异常详细信息 - 在新定义异常时,需要构造无参和一个字符串参数的初始化器,字符串参数会传递给超类构造器
super()
4.5 异常捕获
- 如果想要处理一个异常,而不是终止程序,就可以使用
try{ code }catch(ExceptionType e){ handler for this type }处理异常 - 如果是一个覆盖超类的方法,而超类没有抛出异常,你就必须捕获每一个检查型异常,不允许在子类throw一个未在超类列出的异常
- 可以在一个try后面使用多个catch块捕获多个异常,对每个异常做不同处理,甚至是抛出
- finally子句,无论如何,在结束try-catch块内容时,都会执行的代码内容,一般用于关闭通道或打开的资源,如果在返回时,执行finally语句,其中有另一个返回,那么之前的返回值会被覆盖,如果在返回时出现异常,在finally子句里面还有一个返回甚至会吞掉这个异常,finally子句要体现在清理资源,不要把改变控制流的语句放在里面(return,break,throw等)
如果在
try或catch中返回了,finally子块直接修改值是不会生效的,修改对象的属性会生效 - try-with-resource语句:最简形式如下,当try块退出时,会自动调用资源类的close方法(如果资源实现了AutoClosable接口,里面只有一个会抛出异常的close方法)
try(Resource res = ...){
work with res
}
// eg:
try(var in = new Scanner(
new FileInputStream("/usr/share/dict/words"),StandardCharsets.utf_8))
{
while(in.hasNext()){
System.out.println(in.next());
}
}- 在java9以后,在try()中可以使用之前定义好了的事实最终变量
- 如果try中出现异常,使用close方法也出现异常,在双try-catch块中,这可能带来问题,而使用try-with-resource块,之前try块的异常会抛出而close方法的异常会被抑制,这些异常将会自动捕获,并由addSuppressed方法增加到原来的异常,如果你对try-with-resource的所有异常感兴趣,可以调用getSuppressed方法抛出被抑制的异常
- 只有是使用资源,尽量使用这种方法关闭资源
4.6 分析程序的栈堆轨迹
- 栈堆轨迹是程序执行过程中,某一时间点上所有挂起方法的调用的一个列表,当java因为未捕获一个异常而终止时,就会显示栈堆轨迹
- 调用Trowable类的非静态printStackTrace方法可以访问栈堆轨迹的文本描述信息
var t = new Trowable();
var out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();- 更灵活的方法是使用StacWalker类,它会生成一个Stackwalker.StackFrame实例流,利用foreach函数传递一个流,进行调用,使用Stackwalker.StackFrame类可以得到执行代码的文件名和行号,使用toString会得到一个格式化字符串,包含这些信息
在java9之前,Throwable.getStackTrace方法会生成一个StackTraceElement[]数组,包含整个堆栈的信息(与Stackwalker.StackFrame实例流类似的信息),它效率不高,且无法访问类对象,只能访问挂起方法的类名
4.7 异常的使用技巧
- 异常处理不能代替测试,异常捕获的时间花费大大超过简单的判断花费的时间
- 不要过分细化异常,尽量合并相邻的有直接关系的try块,减少代码量,使代码清晰
- 充分利用异常的层次结构,不要只是抛出RuntimeException异常或只捕获Throwable异常;尽量不要因为逻辑错误,抛出检查型异常,不然可能经常要捕获在已知这种情况下不可能发生的异常;可以适当将异常转换成更合适的异常
- 不要压制异常,如果异常很重要应该进行处理
- 在检查错误时,苛刻比放任更好,如果出现异常情况,最好抛出,而不是放任,比如:弹栈操作pop,如果栈为空,是应该返回null还是抛出异常,最好在出错的地方抛出一个EmptyStrackException异常,好过之后访问时,出现NullPointerExecption异常
- 不要羞愧于传递异常,这样可以把异常传递给更高层的方法,通知用户发生了错误,或直接放弃指令
5号和6号可归纳为早抛出,晚捕获
4.8 断言
- 在java中使用异常检查会让程序慢很多,而且测试完成后会一直保留,断言允许在测试中插入一些检查,并且在生成最终代码时会自动删除这些检查
- 定义一个断言:
assert condition: expression;,例如:assert x >= 0;或assert x >= 0: x,如果x>=0不成立,expression部分就会产生一个消息字符串,传入AssertionError的构造器中,这个表达式一般为字符串或出错的对象值,一般情况下也不会使用,因为如果使用,就会鼓励程序员在之后恢复程序运行,不符合断言设计初衷 - 启用禁用全部断言:在默认情况下,断言是禁用的,如果需要,运行时可使用
-enableassertions或-ea启用断言,例如:java -enableassertions:包及子包(或类及子类) class文件,当然也可以使用-disableassertions或-da,禁用断言
对于系统类这种不使用类加载器加载的类,需使用
-enablestemassertions/-esa命令打开断言,对于使用类加载器加载的类,也可以使用类加载器编程控制,在java.lang.Classloader库中有相应的方法
使用断言完成参数检查:断言是失败的不可恢复的错误,仅在测试时使用,不应该使用断言通知恢复性错误
- 对于方法参数而言,可以使用断言检查方法是否在合适的范围内,这种断言约定被称为前置条件(precontion),承诺在任何情况下都有正确的行为
- 如果调用的参数在错误的范围,它的行为是难以预料的,如果测试调用时没有满足前置条件,断言会失败
使用断言来提供假设文档,对于if语句,在不执行if转而执行else语句的话,可以使用注释提供假设文档,例如:
if(i % 3 == 0){
...
}else if(i % 3 == 1){
...
}else{ //i % 3 == 2
//对于注释部分可以使用断言更好
assert i % 3 == 2;
...
}
//因为java中,%号的定义为 (y / x) * x + y % x = y,这对于负数而言,会得到负的余数
//由于一些未考虑到的情况,if语句最后的else可能不止你所考虑的情况,因此使用断言判断更好4.9 日志
- 每个程序员都熟悉在程序中间插入输出语句检查程序运行的行为,一旦发现,就要删除,如果又出现,又要插入
- 使用日志API可以很容易地开启取消不同等级的记录,并且可以将它存入xml文件中,也可以使用不同的记录配置
- 基本日志:使用全局日志记录器(global logger)并调用info方法:
logger.getGlobal().info("操作名")就可以记录该操作,在控制台显示出来,在前面使用logger.getGlobal().setLevel(Level.OFF)可以关闭所有日志,可以在main函数外面
- 高级日志:你肯定不希望所有的消息记录到同一个日志记录器中,可以使用
public static final Logger 对象名 = Logger.getLogger("字符串,一般为包名"),定义为static是因为java的垃圾回收机制,如果没有使用该变量,最终会被回收 - 日志有七个级别,从高到低为
SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST,在默认情况下,只记录前三个级别,可以使用setLevel(Level.FINE)函数记录不同的级别(比它高的),也可以使用Level.ALL开启所有级别记录,Level.OFF关闭所有 - 当然,对于某一条可以使用log(Level.FINE,message)限定等级,它会显示调用堆栈得出的包含日志调用的类名和方法名
- 如果是虚拟机优化了执行过程的话,就无法得到准确的信息,就应该使用
logp(Level l,String clssName,String methodName,String message)方法,在日志类里还有追踪流的方法enter和exiting - 可以使用其中的
throwing(string className,string methodName,Throwable t)或log方法抛出异常 - 修改日志管理器:在java 9版本以后,日志管理器的配置文件位于
conf/logging.properties,可以修改文件或使用java -Djava.util.logging.config.file = configFile MainClass修改使用的配置文件 - 在配置文件中,.level = INF0确定了默认日志级别,可以使用之前定义高级日志记录器对象使用的字符串.level = 级别修改记录器级别,记录器只负责生成记录,发送消息是处理器的任务,如果希望显示FINE级别及以下的消息,就需要
java.util.logging.ConsoleHander.level = FINE,日志属性文件也可以由LogManager类处理
日志消息的本地化:如果你制作的是全球化的应用程序,你可能希望日志文件全球用户都可以阅读,就应该在程序中使用多种语言的字符串资源包,将其命名为
logmessage_en.properties文件中,其中en是编码,将他们与类文件放在一起,以便ResourceBundle类自动找到他们,这些文件是一个个定义的字符串(类似C++的define,会在日志记录器使用时,将键切换成对应的值,键值中间使用=号替换),包含占位符,便于之后传递值,在初始化getLogger时指定日志记录器使用的资源包创建时,创建一些的资源包,存储本地的特定信息,然后为资源包添加映射,映射与语言相关,一般存储在命名为
logmessages_en.properties文件中,en是英文编码,将不同语言的映射放在一个文件夹下,ResourceBundle类能够自动找到他们,这些文件都是纯文本文件,在使用Logger.getLogger()函数时,除了LoggerName外,还可以传入一个资源包字符串(例如:com.myCompany.logmessages),然后为资源包指定键,一般使用键名 = 值的形式使用时,创建一个
Logger logger = Logger.getLogger("日志名称","资源包名"),使用它的log或info这样的发送日志函数,传入键值,如果希望传值入键(键对应的值使用了占位符{0})而且需要传的值较多,应该使用logb函数,它的最后一个参数是Object的可变参数列表,如果需要切换日志消息资源包,就可以使用Locale.setDefault(Locale.国家名)方法更改使用的资源包附本地化实例图:

处理器:在默认情况下,日志记录器会将记录发送到ConsoleHandler中,并由他输出到System.err流中,处理器也有日志级别,只有同时高于两者级别的日志才会被记录,这个级别可以由日志管理器配置文件设置:
java.util.logging.ConsoleHandler.level = INFO当然和日志记录器一样,绕过配置文件可以安装自己的处理器,使用自定义的
ConsoleHandler对象.setLevel(Level.FINE)方法可以设置处理器的级别,再使用日志记录器的addHandler()方法,使用新的处理器在默认的情况下,日志处理器和它的父处理器会同时发挥作用,所有新处理器的祖先都是名为""的处理器,如果不希望重要的消息出现两遍,就应该讲处理器的
useParentHandler属性设置为false如果希望将日志记录发送到其他地方,就要添加其他的处理器,日志API中提供了两个有用的处理器,
FileHandler和SocketHandler,一个可以把日志添加到XMl格式文件中,一个可以把日志发送到指定的主机或端口,日志的存储可以开启循环功能,日志文件一般以javan.log文件命名(n = 0,1,2,3...),这些文件会被发送到用户的主目录下(没有主目录存储在默认位置c:/Window)日志文件自命名,可使用如下表达式

文件处理器的参数:

日志处理器的扩展:如果希望在其它地方显示日志,就可以使用
Handler类或StreamHandler类自定义处理器,使用流的方法输出到显示区域内,重写publish、flush、close方法过滤器:默认情况下,会根据日志级别进行过滤,每个日志记录器和处理器都有一个可选的过滤器来完成附加的过滤,这个过滤器要实现
Filter接口的isLoggable(LogRecord)方法,这个方法应该对你想要保留的日志返回true,LogRecord对象有getMessage()方法可以返回日志信息字符串,用于分析日志信息,比如可以分析是否以特定单词开头格式化器:如果你不希望生成XML或文本格式的文件,可以自定义格式,扩展Fomatter类并覆盖
format(LogRecord)方法,这个方法将消息部分格式化,替换参数应用于本地化处理,再覆盖getHead(Handler)方法和getTail(Handler)方法,用于生成格式化头和尾,最后使用setFormatter()方法设置日志技巧:
- 对于有大量活动的类,可以使用
private static final Logger,并用包名命名 - 默认的配置会把INFO及以上的消息发送,用户可以覆盖这个配置,但改变过程有点复杂,因此最好在你的应用中安装一个更合适的配置
- INFO、WARNING和SEVERE的消息都会显示到控制台上,最好只将有意义的消息设置成这些级别,一般消息设置为FINE级别是个好选择
- 对于有大量活动的类,可以使用
4.10 调试技巧
- 如果编写了一个程序,认为恰当得捕获了所有异常,但是运行时程序异常,就应该使用调试,如果没有一个强大的编译器,就可以尝试以下技巧
- 使用打印或记录的方法把任意变量的值记录下来
- 在每个class文件中使用main函数进行测试,使用这样的单元测试桩独立测试使用的类
- 使用JUnit测试套件,它可以容易地组织测试用例,可以使用测试用例进行运行测试
- 日志代理,使用匿名子类(生成一个类的对象,在对象{}中创建匿名子类,定义方法,在方法中调用该类(super)的方法,并记录返回值等信息到日志中)插入日志记录堆栈轨迹
- 使用Trowable类的printStackTrace方法打印栈堆轨迹,然后重新抛出异常,以便找到相应的处理器,当然也可以使用dumpStack方法获得栈堆轨迹
- 在System.err中获取栈堆轨迹,并把它使用PrintWrite(StringWriter)对象记录下来
- 使用命令将程序错误记录到文件中
- 将栈堆轨迹使用Thread.setDefaultUncaughtExceptionHandler()方法记录到文件中
- 使用java -verbose观察类的加载过程
- 使用javac -Xlint告诉编译器寻找常见问题(比如switch缺少break语句),使用javac --help - X命令查看所有警告列表
- java虚拟机增加了对应用程序的监控和管理功能,可以使用jdk中的jconsole的图形工具显示有关虚拟机性能的统计结果,从正在运行的java程序列表中选择需要调试的程序
- java任务控制器(java Misson Control)是一个专业级性能分析和诊断工具,能关联到正在运行的虚拟机,分析java飞行记录器(java Flight Recorder)的输出,从正在运行的java应用程序收集诊断和性能分析数据
