第七章 异常、断言和日志
7. 异常、断言和日志
7.1 处理错误
异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。
考虑到程序中可能会出现的错误和问题,需要考虑这些问题:
- 用户输入错误
- 设备错误
- 物理限制
- 代码错误
7.1.1 异常分类
异常对象都是派生于Throwable 类的一个实例。
Error 类层次结构描述了Java 运行时系统的内部错误和资源耗尽错误。
在设计Java 程序时, 需要关注Exception 层次结构。这个层次结构又分解为两个分支:
-
一个分支派生于RuntimeException 。由程序错误导致的异常属于RuntimeException 。
如果出现RuntimeException 异常, 那么就一定是你的问题。
包括:- 错误的类型转换
- 数组访问越界
- 访问null 指针
-
另一个分支包含其他异常。程序本身没有问题, 但由于像I/O 错误这类问题导致的异常属于其他异常。包括:
- 试图在文件尾部后面读取数据。
- 试图打开一个不存在的文件。
- 试图根据给定的字符串查找Class 对象, 而这个字符串表示的类并不存在。
Java 语言规范将派生于Error 类或RuntimeException 类的所有异常称为非受查( unchecked ) 异常, 所有其他的异常称为受查( checked) 异常。编译器将核查是否为所有的受査异常提供了异常处理器。
7.1.2 声明受查异常
需要记住在遇到下面4 种情况时应该抛出异常:
- 调用一个抛出受査异常的方法, 例如, FilelnputStream 构造器。
- 程序运行过程中发现错误, 并且利用throw 语句抛出一个受查异常
- 程序出现错误, 例如,a[-1]=0 会抛出一个ArraylndexOutOffloundsException 这样的非受查异常
- Java 虚拟机和运行时库出现的内部错误。
总之,一个方法必须声明所有可能抛出的受查异常, 而非受查异常要么不可控制( Error),要么就应该避免发生( RuntimeException )。如果方法没有声明所有可能发生的受查异常, 编译器就会发出一个错误消息。
如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方法中声明的异常更通用。如果超类方法没有抛出任何受查异常, 子类也不能抛出任何受查异常。
7.1.3 如何抛出异常
EOFException e = new EOFExceptio() ;
throw e;
String readData(Scanner in) throws EOFException
{
...
while (...)
{
if (!in.hasNext()) // EOF encountered
{
if(n < len)
throw new EOFException();
}
...
}
return s;
}
对于一个已经存在的异常类, 将其抛出非常容易D,在这种情况下:
1 ) 找到一个合适的异常类。
2 ) 创建这个类的一个对象。
3 ) 将对象抛出。
一旦方法抛出了异常, 这个方法就不可能返回到调用者。
7.1.4 创建异常类
在遇到任何标准异常类都没有能够充分地描述清楚的问题时。在这种情况下, 要创建自己的异常类。
定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息的构造器(超类Throwable 的toString 方法将会打印出这些详细信息, 这在调试中非常有用)。
class FileFormatException extends IOException
{
public FileFormatExceptionO {}
public FileFormatException(String gripe)
{
super(gripe);
}
}
抛出自己定义的异常类型
String readData(BufferedReader in) throws FileFormatException
{
...
while (. . .)
{
if (ch == -1) // EOF encountered
{
if (n < len)
throw new FileFornatException();
}
...
}
return s;
}
7.2 捕获异常
7.2.1 捕获异常
try/catch 语句块
如果在try 语句块中的任何代码抛出了一个在catch 子句中说明的异常类,则程序将跳过try 语句块的其余代码,执行catch 子句中的处理器代码。如果在try 语句块中的代码没有拋出任何异常,那么程序将跳过catch 子句。
public void read(String filename)
{
try
{
InputStream in = new Filei叩utStream(filename) ;
int b;
while ((b = in.read()3 != -1)
{
process input
}
}
catch (IOException exception)
{
exception.printStackTrace();
}
}
read 方法有可能拋出一个IOException 异常。在这种情况下,将跳出整个while 循环,进入catch 子旬,并生成一个栈轨迹。
其他处理方法:read 方法出现了错误, 就让read 方法的调用者去操心,如果采用这种处理方式,就必须声明这个方法可能会拋出一个IOException。
public void read(String filename) throws IOException
(
InputStream in = new FileInputStream(filename) ;
int b;
while ((b = in.readO) != -1)
{
process input
}
}
- 哪种方法更好呢? 规则是:通常, 应该捕获那些知道如何处理的异常, 而将那些不知道怎样处理的异常继续进行传递。传递异常也就是在方法的首部添加一个throws 说明符,以便告知调用者这个方法可能会抛出异常。
例外:如果编写一个覆盖超类的方法,而这个方法又没有抛出异常(如JComponent 中的paintComponent ), 那么这个方法就必须捕获方法代码中出现的每一个受查异常。不允许在子类的throws 说明符中出现超过超类方法所
列出的异常类范围。
7.2.2 捕获多个异常
为每个异常类型使用一个单独的catch 子句
异常对象可能包含与异常本身有关的信息:
-
得到详细的错误信息(如果有的话):
e.getHessage()
-
得到异常对象的实际类型:
e.getClass().getName()
同一个catch 子句中可以捕获多个异常类,只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。捕获多个异常时, 异常变量隐含为final 变量
7.2.3 再次抛出异常与异常链
将原始异常设置为新异常的“ 原因”:
try
{
access the database
}
catch (SQLException e)
{
Throwable se = new ServletException ("database error");
se.initCause(e);
throw se;
}
当捕获到异常时, 就可以使用
Throwable e = se.getCause()
重新得到原始异常:
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
7.2.4 finally 子句
Java 7 之后,try-with-resources
语句可能比finally
子句更加实用
不管是否有异常被捕获, finally 子句中的代码都被执行。例如:要关闭输入流。
try 语句可以只有finally 子句,而没有catch 子句。
- 强烈建议解遇合
try/catch
和try/finally
语句块。这样可以提高代码的清晰度:
InputStrean in = . . .;
try
{
try
{
code that might throw exceptions
}
finally
{
in.close();
}
}
catch (IOException e)
{
show error message
}
内层的try 语句块只有一个职责, 就是确保关闭输入流。外层的try 语句块也只有一个职责, 就是确保报告出现的错误。这种设计方式不仅清楚, 而且还具有一个功能, 就是将会报告finally 子句中出现的错误。
-
当finally 子句包含return 语句时:假设利用return语句从try 语句块中退出。在方法返回前, finally 子句的内容将被执行。如果finally 子句中也有一个return 语句, 这个返回值将会覆盖原始的返回值。
finally子句的体要用于清理资源。不要把改变控制流的语句(
return, throw, break, continue
)放到finally子句中。
7.2.5 带资源的try语句
带资源的try 语句(try-with-resources ) 的最简形式为:
try (Resource res = . . .)
{
work with res
}
try 块退出时,会自动调用res.close()。
例子:读取文件中的所有单词
不论这个块如何退出, in 和out 都会关闭。
try (Scanner in = new Scanner(new FileInputStream('7usr/share/dict/words"). "UTF-8"): PrintWriter out = new PrintWriter("out.txt")) { while (in.hasNext()) out.println(in.next() .toUpperCase()) ; }
只要需要关闭资源, 就要尽可能使用带资源的try语句。
带资源的try 语句自身也可以有catch 子句和一个finally 子句。这些子句会在关闭资源之后执行。不过在实际中, 一个try 语句中加入这么多内容可能不是一个好主意。
7.2.6 分析堆栈轨迹元素
堆栈轨迹( stack trace ) 是一个方法调用过程的列表, 它包含了程序执行过程中方法调用的特定位置。当Java 程序正常终止, 而没有捕获异常时, 这个列表就会显示出来。
可以调用Throwable 类的printStackTrace 方法访问堆栈轨迹的文本描述信息
Throwable t = new Throwable();
StringWriter out = new StringWriter() ;
t.printStackTrace(new PrintWriter(out)) ;
String description = out.toString() ;
一种更灵活的方法是使用StackWalker类
7.3 使用异常机制的技巧
下面给出使用异常机制的几个技巧:
-
异常处理不能代替简单的测试
只有在异常情况下使用异常
-
不要过分地细化异常
异常处理机制的其中一个目标,将正常处理与错误处理分开
-
利用异常层次结构
不要只抛出RuntimeException 异常。应该寻找更加适当的子类或创建自己的异常类。
不要只捕获Thowable 异常, 否则,会使程序代码更难读、更难维护。
考虑受查异常与非受查异常的区别。
将一种异常转换成另一种更加适合的异常时不要犹豫。
-
不要压制异常
-
在检测错误时,“ 苛刻” 要比放任更好。
-
不要羞于传递异常。
5、6 可以归纳为“ 早抛出, 晚捕获“。
7.4 使用断言
7.4.1 断言的概念
假设确信某个属性符合要求, 并且代码的执行依赖于这个属性。
断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插人的检测语句将会被自动地移走。
要想断言?c 是一个非负数值:
assert x >= 0;
或者将x 的实际值传递给AssertionError 对象, 从而可以在后面显示出来。
assert x >= 0 : x;
7.4.2 启用和禁用断言
在默认情况下, 断言被禁用。可以在运行程序时用-enableassertions 或-ea 选项启用:
java -enableassertions MyApp
在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器( class loader ) 的功能。当断言被禁用时, 类加载器将跳过断言代码, 因此,不会降低程序运行的速度。
也可以在某个类或整个包中使用断言:
java -ea:MyClass -eaiconi .inycompany.inylib. . . MyApp
这条命令将开启MyClass 类以及在com.mycompany.mylib 包和它的子包中的所有类的断言
7.4.3 使用断言完成参数检查
Java中3 种处理系统错误的机制:
- 抛出一个异常
- 日志
- 使用断言
什么时候用断言呢?
- 断言失败是致命的、不可恢复的错误。
- 断言检查只用于开发和测阶段(在靠近海岸时穿上救生衣,但在海中央时就把救生衣抛掉吧”)
不应该使用断言向程序的其他部分通告发生了可恢复性的错误, 或者,不应该作为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置。
7.4.4 为文档假设使用断言
很多程序员使用注释说明假设条件。可以用断言。
断言是一种测试和调试阶段所使用的战术性工具; 而日志记录是一种在程序的整个生命周期都可以使用的策略性工具。
7.5 记录曰志
日志API的优点:
- 可以很容易地取消全部日志记录, 或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
- 可以很简单地禁止日志记录的输出, 因此,将这些日志代码留在程序中的开销很小。
- 日志记录可以被定向到不同的处理器, 用于在控制台中显示, 用于存储在文件中等。
- 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准
丢弃那些无用的记录项。 - 日志记录可以采用不同的方式格式化,例如,纯文本或XML。
- 应用程序可以使用多个日志记录器, 它们使用类似包名的这种具有层次结构的名字,例如, com.mycompany.myapp。
- 在默认情况下, 日志系统的配置由配置文件控制。如果需要的话, 应用程序可以替换这个配置。
7.5.1 基本曰志
要生成简单的日志记录,可以使用全局日志记录器(global logger) 并调用其info 方法:
Logger.getClobal 0,info("File->Open menu item selected");
会显示
May 10, 2013 10:12:15 PM LogginglmageViewer fileOpen
INFO: File->0pen menu item selected
如果在适当的地方(如main 开始)调用:
Logger.getClobal () .setLevel (Level .OFF) ;
将会取消所有的日志。
7.5.2 高级曰志
调用getLogger
方法创建或获取记录器:
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp") :
用一个静态变量存储日志记录器的一个引用,因为未被任何变量引用的日志记录器可能会被垃圾回收。
日志记录器名的层次结构:
通常, 有以下7 个日志记录器级别:
• SEVERE
• WARNING
• INFO
• CONFIG
• FINE
• FINER
• FINEST
在默认情况下, 只记录前3个级别
logger,setLevel (Level .FINE) ;//FINE 和更高级别的记录都记录下来
logger.log(Level .FINE, message);//使用log 方法指定级别
logger.warning(message)://对于所有的级别
logger.fine(message) ;//对于所有的级别
Level.ALL 开启所有级别的记录, 或者使用Level.OFF 关闭所有级别的记录。
记录日志的常见用途是记录那些不可预料的异常。
7.5.3 修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。
日志管理器在虚拟机启动时初始化, 这在main 执行之前完成
在Java9中,通过调用以下方法更新日志配置:
LogManager.getLogManager().updateConfiguration(mapper);
7.5.4 本地化
本地化的应用程序包含资源包( resource bundle ) 中的本地特定信息。
在请求日志记录器时,可以指定一资源包:
Logger logger = Logger.getLogger(1oggerName, "com.mycompany.1ogmessages") ;
7.5.5 处理器
日志记录器将记录发送到ConsoleHandler 中, 并由它输出到System.err流中。
与日志记录器一样, 处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。
在默认情况下, 日志记录器将记录发送到自己的处理器和父处理器。我们的日志记录器是原始日志记录器(命名为“ ”)的子类, 而原始日志记录器将会把所有等于或高于INFO级別的记录发送到控制台。
要想将日志记录发送到其他地方, 就要添加其他的处理器。日志API为此提供了两个很有用的处理器,:
- FileHandler :收集文件中的记录
- SocketHandler:将记录发送到特定的主机和端口
可以像下面这样直接将记录发送到默认文件的处理器:
FileHandler handler = new FileHandler() ;
1ogger.addHandler(handler) ;
如果多个应用程序(或者同一个应用程序的多个副本)使用同一个口志文件, 就应该开启append 标志。另外, 应该在文件名模式中使用%u(用于解决冲突的唯一编号), 以便每个应用程序创建日志的唯一副本。
开启文件循环功能也是一个不错的主意。日志文件以myapp.log.O, myapp.log.1 , myapp.log.2, 这种循环序列的形式出现3 只要文件超出了大小限制, 最旧的文件就会被删除, 其他的文件将重新命名, 同时创建一个新文件, 其编号为0。
还可以通过扩展Handler 类或StreamHandler 类自定义处理器,这个处理器将在窗口中显示日志记录。
7.5.6 过滤器
在默认情况下, 过滤器根据日志记录的级别进行过滤。
可以通过实现niter 接口并定义下列方法来自定义过滤器。
boolean isLoggab1e(LogRecord record)
要想将一个过滤器安装到一个日志记录器或处理器中, 只需要调用setFilter 方法就可以了。注意,同一时刻最多只能有一个过滤器。
7.5.7 格式化器
ConsoleHandler 类和FileHandler 类可以生成文本和XML 格式的日志记录
7.5.8 日志记录说明☆
总结了一些最常用的操作:
-
为一个简单的应用程序, 选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字, 例如,com.mycompany.myprog。可以通过调用下列方法得到日志记录器:
Logger logger = Logger.getLogger("com.mycompany.myprog");
为方便起见,可能希望利用一些日志操作将下面的静态域添加到类中:
private static final Logger logger = Logger.getLogger("com.mycompany.nyprog"):
-
默认的日志配置将级别等于或高于INFO 级别的所有消息记录到控制台。
下列代码确保将所有的消息记录到应用程序特定的文件中。可以将这段代码放置在应用程序的main 方法中。
if (System,getProperty("java,util.logging.config.dass") == null && System.getPropertyC'java.util.logging.config.file") == null) { try { Logger.getLogger("").setLevel (Level .ALL); final int L0C_R0TATI0N_C0UNT = 10; Handler handler = new FileHandler('Wmyapp.log", 0, L0G_R0TATI0N_C0UNT): Logger.getLogger("").addHandler(handler): } catch (IOException e) { logger.log(Level .SEVERE, "Can't create log file handler", e); } }
-
现在,可以记录自己想要的内容了。但需要牢记: 所有级别为INFO、WARNING 和SEVERE 的消息都将显示到控制台上。因此, 最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录,设定为FINE 是一个很好的选择。
当调用System.out.println 时, 实际上生成了下面的日志消息:
logger.fine("File open dialog canceled");
记录那些不可预料的异常也是一个不错的想法, 例如:
try { ... } catch (SonreException e) { logger,log(Level .FINE, "explanation", e); }
7.6 调试技巧
-
打印或记录任意变量的值:
System.out.println("x=" + x);
或者
Logger.getClobal0-info(nx=" + x);
要想获得隐式参数对象的状态,就可以打印this 对象的状态:
Logger.getClobal().info("this=" + this);
-
在每一个类中放置一个单独的main 方法。这样就可以对每一个类进行单元测试。
利用这种技巧, 只需要创建少量的对象, 调用所有的方法, 并检测每个方法是否能够正确地运行就可以了。另外, 可以为每个类保留一个main 方法,然后分别为每个文件调用Java 虚拟机进行运行测试。在运行applet 应用程序的时候, 这些main 方法不会被调用, 而在运行应用程序的时候,Java 虚拟机只调用启动类的main 方法。
-
JUiiit 是一个非常常见的单元测试框架, 利用它可以很容易地组织测试用例套件。
-
日志代理( logging proxy) 是一个子类的对象, 它可以截获方法调用, 并进行日志记录, 然后调用超类中的方法。
-
利用Throwable 类提供的printStackTmce 方法,可以从任何一个异常对象中获得堆栈情况。
-
—般来说, 堆栈轨迹显示在System.err 上。也可以利用
printStackTrace(PrintWriter s)
方法将它发送到一个文件中。 -
将一个程序中的错误信息保存在一个文件中
-
让非捕获异常的堆栈轨迹出现在System.err 中并不是一个很理想的方法。
-
要想观察类的加载过程, 可以用-verbose 标志启动Java 虚拟机。
-
-Xlint 选项告诉编译器对一些普遍容易出现的代码问题进行检査。
-
java 虚拟机增加了对Java 应用程序进行监控(monitoring) 和管理(management ) 的支持。它允许利用虚拟机中的代理装置跟踪内存消耗、线程使用、类加载等情况。这个功能对于像应用程序服务器这样大型的、长时间运行的Java 程序来说特别重要。
-
可以使用jmap 实用工具获得一个堆的转储, 其中显示了堆中的每个对象。
-
如果使用-Xprof 标志运行Java 虚拟机, 就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。
欢迎关注公众号:GitKid,暂时每日分享LeetCode题解,在不断地学习中,公众号内容也在不断充实,欢迎扫码关注