Java基础笔记(3)-异常、断言和日志
异常
异常介绍
Java中异常整体的架构图如下:
在Java中,异常对象都是派生于Throwable类的一个类实例。当然用户也可以创建自己的异常类。所有异常都是由Throwable继承而来,然后分解为两个分支:
- Error(错误):Java虚拟机无法解决的严重问题,例如JVM系统内部错误,资源耗尽等
- Exception(异常):其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码来进行处理,例如空指针访问等
在编写Java程序的时候,我们重点管制的时Exception层次,这里又分为两个分支,一个分支派生于RuntimeException,另一个则是其他异常。
由编程错误导致的异常属于RuntimeException,例如:
- 错误的强制类型转换
- 数组访问越界
- 空指针异常
程序本身没有问题,但是由于像IO错误这类问题导致的异常属于其他异常,例如:
- 试图超越文件末尾继续读取数据
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找Class对象,但是这个字符串表示的类并不存在
在Java语言规范中,将派生于Error类或者RuntimeException类的所有异常称为非检查型(unchecked)异常,其他的异常称为检查型(checked)异常。所有检查型异常都应该在代码中处理,处理包括捕获或者抛出。
自定义异常
当程序中出现了某些“错误”,但是这些错误信息并没有在Throwable子类中进行描述处理,这个时候可以自己设计异常类,用于描述该错误信息。实现自定义异常只需要定义一个派生于Exception的类,或者派生于Exception的某个子类。
习惯的做法是,自定义的这个类应该包含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器。
Throwable的toString方法会返回一个字符串,其中包含了这个详细信息,同时还提供getMessage()方法,用于获得Throwable对象的详细描述信息。
异常处理机制
异常处理机制包括捕获和抛出。
抛出指的是throws
关键字。如果使用抛出来处理异常,那么一个方法应该声明所有可能抛出的检查型异常。
throws和throw的对比:
throws:用来声明一个方法可能产生的所有异常
throw:用来抛出一个具体的异常对象
throws throw 意义 异常处理的一种方式 手动生成异常对象的关键字 位置 方法声明处 方法体中 后面带的东西 异常类型 异常对象
- 注意:子类重写父类的方法的时候,对抛出异常的规定:子类重写的方法,所抛出的异常类型要么和父类抛出的异常一致,要么是父类抛出异常类型的子类型
捕获指的是try-catch-finally
。在代码中捕获了该异常,就无需继续抛出了。
1 |
|
多个异常的捕获
1 |
|
- 捕获多个异常的时候,异常变量隐含为final变量
- 可以在catch子句中再次抛出异常,这通常用来改变异常的类型
- 不应该在finally代码块中包含return语句,否则返回值会进行覆盖,甚至会吞掉可能的异常
堆栈轨迹(Stack Trace)是程序执行过程中某个特定点上所有挂起方法调用的一个列表。
可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息
try-with-resources
在finally语句中我们一般执行一些清除资源的流程,try-with-resources语句也能够达到相同的效果。它的最简语法如下:
1 |
|
上面的代码,在try代码块执行完毕之后,会自动调用res.close()
方法。当然这要求我们使用的资源类都实现了AutoCloseable
接口,在这个接口中提供的方法void close() throws Exception
- 在括号中也可以指定多个资源,即使用多个语句定义资源,语句之间使用分号
;
分开 - try-with-resources语句本身也可以有catch子句和finally子句
- 如果try块抛出异常,close方法也抛出异常,那么原来的异常会重新抛出,而close方法抛出的异常会被抑制。这些异常会被自动捕获,并由addSuppreseed方法增加到原来的异常。调用getSuppressed方法,可以生成从close方法抛出并抑制的异常数组
断言
断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除这些检查。断言对应的关键字是assert
:
1 |
|
上面两个语句都会计算条件,如果结果为false,则抛出一个AssertionError异常。在第二种形式中,表达式expression将传入AssertionError对象的构造器,转化成一个消息字符串。
表达式的唯一目的是产生一个消息字符串。AssertionError对象并不存储具体的表达式值。
在默认情况下,断言是禁用的,可以在运行程序的时候使用选项开启。java -enableassertions
或者java -ea
。我们不必重新编译程序来启动或禁用断言,这是类加载器的职责。禁用断言的时候,类加载器会去除断言代码。
断言也可以是一种错误处理的机制,但是断言使用的情况往往是下面几种:
- 断言失败是致命的,不可恢复的错误
- 断言检查只是在开发和测试阶段才会打开
我们不应该使用断言向程序的其他部分通知错误,不应该利用断言与程序用户沟通问题。断言只应该用于在测试阶段确定程序内部错误的位置。
日志
日志优点
在一些简单的场景下,我们会使用System.out.println
来进行一些输出,观察程序的行为。但是这个输出语句毕竟不是专门用来解决日志问题,日志API相比于它,会有更多的优点:
- 可以很容易地取消其全部日志记录,或者仅仅取消某个级别以下的日志
- 可以很容易地禁止日志记录
- 日志记录可以被定向到不同的处理器
- 日志记录器和处理器都可以对记录进行过滤
- 日志记录可以采用不同的方式进行格式化
- 应用程序可以使用多个日志记录器
- 日志系统的配置由配置文件控制
标准Java日志框架
标准Java日志框架需要引入的包为java.util.logging
下面是一个基本的日志使用方式。
1 |
|
在一个专业的应用程序中,我们不应该将所有的日志都记录到一个全局的日志记录器中,因此我们可以定义自己的日志记录器,使用getLogger
方法创建或者获取日志记录器
1 |
|
日志记录器的名称具有层次结构,父子日志记录器会共享某些属性,如日志级别等。
通常会有下面7个日志级别:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
日志级别从高到底,默认只记录前三个级别的日志。也可以显式地设置日志记录级别,例如logger.setLevel(Level.FINE)
,这样就会记录该级别以及更高级别的日志。也可以使用Level.ALL
开启所有级别的日志记录,Level.OFF
关闭所有级别的日志记录。
可以通过编辑配置文件来修改日志系统的各个属性。默认情况下,配置文件位于conf/logging.properties
目录下,也可以通过设置java.util.logging.config.file
的属性来确定配置文件的位置。
在日志框架中,还有三个比较重要的概念,处理器Handler、过滤器Filter和格式化器Formatter。
在默认情况下,日志记录器将日志记录发送到自己的处理器以及父日志记录器的处理器,最终会达到祖先处理器的ConsoleHandler中,它默认将记录输出到System.err流中,且日志记录等级为INFO。如果想要将日志记录发送到其他地方,就需要添加其他的处理器。日志API中提供了两个有用的Handler,分别是FileHandler,将日志发送到文件中;和SocketHandler,将日志发送到指定的主机和端口。调用addHandler
方法来增加处理器。
在默认情况下,会根据日志记录的级别进行过滤。每个日志记录器和处理器都有一个可选的过滤器来完成附加的过滤。定义过滤器,需要实现Filter接口,并定义方法boolean isLoggable(LogRecord record)
。这个方法对那些应该包含在日志中的记录返回true。使用setFilter
方法来设置过滤器,同一时刻最多只能有一个过滤器。
ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。当然我们也可以自定义格式,这需要继承Formatter类并重写其中的String format(LogRecord record)
方法,根据需要对记录中的信息进行格式化,并返回结果字符串。使用setFormatter
方法来设置格式化器。