Java核心技术笔记 异常、断言和日志

《Java核心技术 卷Ⅰ》 第7章 异常、断言和日志前端

  • 处理错误
  • 捕获异常
  • 使用异常机制的技巧
  • 记录日志

处理错误

若是因为出现错误而是的某些操做没有完成,程序应该:java

  • 返回到一种安全状态,并让用户执行一些其余操做;或者
  • 容许用户保存全部操做,并以妥善方式终止程序

检测(或引起)错误条件的代码一般离:git

  • 能让数据恢复到安全状态
  • 能保存用户的操做结果并正常退出程序

的代码很远。github

异常处理的任务:将控制权从错误产生地方转移给可以处理这种状况的错误处理器数据库

异常分类

在Java中,异常对象都是派生于Throwable类的一个实例,若是Java中内置的异常类不能知足需求,用户还能够建立本身的异常类。编程

Java异常层次结构:数组

  • Throwable
    • Error
      • ...
    • Exception
      • IOException
        • ...
      • RuntimeException
        • ...

能够看到第二层只有ErrorException安全

Error类层次结构描述了Java运行时系统的内部错误资源耗尽错误,应用程序不该该抛出这种类型的对象,这种内部错误的状况不多出现,出现了能作的工做也不多。app

设计Java程序时,需关注Exception层次结构,这个层次又分为两个分支,RuntimeException和包含其余异常的IOException测试

划分两个分支的规则是:

  • 由程序错误致使的异常属于RuntimeException
  • 程序自己无问题,因为像I/O错误这类问题致使的异常属于其余异常IOException

派生于RuntimeException的异常包含下面几种状况:

  • 错误类型转换
  • 数组访问越界
  • 访问null指针

派生于IOException的异常包含下面几种状况:

  • 试图在文件尾部后面读取数据
  • 试图打开一个不存在的文件
  • 试图根据指定字符串查找Class对象,而这个字符串表示的类并不存在

Java语言规范将派生于Exception类和RuntimeException类的全部异常统称非受查(unchecked)异常,全部其余异常都是受查(checked)异常。

编译器将核查是否为全部的受查异常提供了异常处理器

声明受查异常

一个方法不只要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误

异常规范(exception specification):方法应该在其首部声明所可能抛出的异常。

public FileInputStream(String name) throws FileNotFoundException 复制代码

若是这个方法抛出了这样的异常对象,运行时系统会开始搜索异常处理器,以便知道如何处理这个异常对象。

固然不是全部方法都须要声明异常,下面4种状况应该抛出异常:

  1. 调用一个抛出受查异常的方法时
  2. 程序运行过程当中发现错误,而且利用throw语句抛出一个受查异常
  3. 程序出现错误,通常是非受查异常
  4. Java虚拟机和运行时库出现的内部错误

出现前两种状况之一,就必须告诉调用者这个方法可能的异常,由于若是没有处理器捕获这个异常,当前执行的线程就会结束

若是一个方法有多个受查异常类型,就必须在首部列出全部的异常类,异常类之间用逗号隔开:

class MyAnimation {
  ...
  public Image loadImage(String s) throws FileNotFoundException, EOFException {
    ...
  }
}
复制代码

可是不须要声明Java的内部错误,即从Error继承的错误。

关于子类和超类在这部分的问题:

  • 子类方法声明的受查异常不能比超类中方法声明的异常更通用(即子类能抛出更特定的异常或者根本不抛出任何异常)
  • 若是超类没有抛出任何受查异常,子类也不能

若是类中的一个方法声明抛出一个异常,而这个异常是某个特定类的实例时:

  • 这个方法可能抛出一个这个类的异常(好比IOExcetion
  • 或抛出这个类的任意一个子类的异常(好比FileNotFoundException

如何抛出异常

假设程序代码中发生了一些很糟糕的事情。

首先要决定应该抛出什么类型的异常(经过查阅已有异常类的Java API文档)。

抛出异常的语句是:

throw new EOFException();
// 或者
EOFException e = new EOFException();
throw e;
复制代码

一个名为readData的方法正在读取一个首部有信息Content-length: 1024的文件,然而读到733个字符以后文件就结束了,这是一个不正常的状况,但愿抛出一个异常。

String readData(Scanner in) throws EOFException {
  ...
  while(...)
  {
    if(!in.hasNext()) // EOF encountered
    {
      if(n < len)
        throw new EOFException();
    }
    ...
  }
  return s;
}
复制代码

EOFException类还有一个含有一个字符串类型参数的构造器,这个构造器能够更加细致的描述异常出现的状况。

String gripe = "Content-length:" + len + ", Received:" + n;
throw new EOFException(gripe);
复制代码

对于一个已经存在的异常类,将其抛出比较容易:

  1. 找到一个合适的异常类
  2. 建立这个类的一个对象
  3. 将对象抛出

一旦抛出异常,这个方法就不可能返回到调用者,即没必要为返回的默认值或错误代码担心。

建立异常类

实际状况中,可能会遇到任何标准异常类不能充分描述的问题,这时候就应该建立本身的异常类。

须要作的只是定义一个派生于Exception的类,或者派生于Exception子类的类。

习惯上,定义的类应该包含两个构造器:

  • 一个是默认的构造器
  • 另外一个是带有详细描述信息的构造器(超类ThrowabletoString方法会打印出这些信息,在调试中有不少用)
class FileFormatException extends IOException {
  public FileFormatException() {}
  public FileFormatException(String gripe) {
    super(gripe);
  }
}
复制代码

捕获异常

捕获异常

要想捕获一个异常,必须设置try/catch语句块。

try
{
  code
  ...
}
catch(ExceptionType e)
{
  handler for this type
}
复制代码

若是try语句块中任何代码抛出了一个在catch子句中说明的异常类,那么:

  1. 程序将跳过try语句块的其他代码
  2. 程序将执行catch子句中的处理器代码

若是没有代码抛出任何异常,程序跳过catch子句。

若是方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会当即退出

// 读取数据的典型代码
public void read(String filename) {
  try
  {
    InputStream in = new FileInputStream(filename);
    int b;
    while((b = in.read()) != -1)
    {
      // process input
      ...
    }
  }
  catch(IOException exception)
  {
    exception.printStackTrace();
  }
}
复制代码

read方法有可能抛出一个IOException异常,这种状况下,将跳出整个while循环,进入catch子句,并生成一个栈轨迹

还有一种选择就是什么也不作,而是将异常传递给调用者

public void read(String filename) throws IOException {
  InputStream in = new FileInputStream(filename);
  int b;
  while((b = in.read()) != -1)
  {
    // process input
    ...
  }
}
复制代码

编译器严格地执行throws说明符,若是调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递。

两种方式哪一种更好

一般,应该捕获那些知道如何处理的异常,将那些不知道怎么样处理的异常进行传递。

这个规则也有一个例外:若是编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每个受查异常;而且不容许在子类的throws说明符中出现超过超类方法所列出的异常类范围。

捕获多个异常

为每一个异常类型使用一个单独的catch子句:

try
{
  code
  ...
}
catch(FileNotFoundException e)
{
  handler for missing files
}
catch(UnknownHostException e)
{
  handler for unknown hosts
}
catch(IOException e)
{
  handler for all other I/O problems
}
复制代码

异常对象可能包含与异常相关的信息,可使用e.getMessage()得到详细的错误信息,或者使用e.getClass().getName()获得异常对象的实际类型。

在Java SE 7中,同一个catch子句中能够捕获多个异常类型,若是动做同样,能够合并catch子句:

try
{
  code
  ...
}
catch(FileNotFoundException | UnknownHostException e)
{
  handler for missing files and unknown hosts
}
catch(IOException e)
{
  handler for all other I/O problems
}
复制代码

只有当捕获的异常类型彼此之间不存在子类关系时才须要这个特性。

再次抛出异常与异常链

catch子句中能够抛出一个异常,这样作的目的是改变异常的类型

try
{
  access the database
}
catch(SQLException e)
{
  throws new ServletException("database error:" + e.getMessage());
}
复制代码

ServletException用带有异常信息文本的构造器来构造。

不过还有一种更好的处理方法,并将原始异常设置为新异常的“缘由”

try
{
  access the database
}
catch(SQLException e)
{
  Throwable se = new ServletException("database error");
  se.initCause(e);
  throw se;
}
复制代码

当捕获到异常时,可使用下面这条语句从新获得原始异常:

Throwable e = se.getCause();
复制代码

这样可让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

finally子句

当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。

若是方法得到了一些本地资源,而且只有这个方法本身知道,又若是这些资源在退出方法以前必须被回收(好比数据库链接的关闭),那么就会产生资源回收问题。

一种是捕获并从新抛出全部异常,这种须要在两个地方清除所分配的资源,一个在正常代码中,另外一个在异常代码中。

Java有一种更好地解决方案,就是finally子句。

无论是否有异常被捕获,finally子句的代码都会被执行。

InputStream in = new FileInputStream(...);
try
{
  // 1
  code that might throw exception
  // 2
}
catch(IOException e)
{
  // 3
  show error message
  // 4
}
finally
{
  // 5
  in.close();
}
// 6
复制代码

上面的代码中,有3种状况会执行finally子句:

  1. 代码没有抛出异常,执行序列为一、二、五、6
  2. 抛出一个在catch子句中捕获的异常
    1. 若是catch子句没有抛出异常,执行序列为一、三、四、五、6
    2. 若是catch子句抛出一个异常,异常将被抛回这个方法的调用者,执行序列为一、三、5(注意没有6)
  3. 代码抛出了一个异常,可是这个异常没有被捕获,执行序列为一、5

try语句能够只有finally子句,没有catch子句。

有时候finally子句也会带来麻烦,好比清理资源时也可能抛出异常。

若是在try中发生了异常,而且被catch捕获了异常,而后在finally中进行处理资源时若是又发生了异常,那么原有的异常将会丢失,转而抛出finally中处理的异常。

这个时候的一种解决办法是用局部变量Exception ex暂存catch中的异常:

  • try中进行执行的时候加入嵌套的try/catch,并在catch中暂存ex并向上抛出
  • finally中处理资源的时候加入嵌套的try/catch,而且在catch中进行判断ex是否存在来进一步处理
InputStream in = ...;
Exception ex = null;
try
{
  try
  {
    code that might throw exception
  }
  catch(Exception e)
  {
    ex = e;
    throw e;
  }
}
finally
{
  try
  {
    in.close();
  }
  catch(Exception e)
  {
    if(ex == null)throw e;
  }
}
复制代码

下一节会介绍,Java SE 7中关闭资源的处理会容易不少。

带资源的try语句

对于如下代码模式:

open a resource
try
{
  work with the resource
}
finally
{
  close the resource
}
复制代码

假设资源属于一个实现了AutoCloseable接口的类,Java SE 7位这种代码提供了一个颇有用的快捷方式,AutoCloseable接口有一个方法:

void close() throws Exception 复制代码

带资源的try语句的最简形式为:

try(Resource res = ...)
{
  work with res
}
复制代码

try块退出时,会自动调用res.close()

try(Scanner in = new Scanner(new FileInputStream("..."), "UTF-8"))
{
  while(in.hasNext())
    System.out.println(in.next());
}
复制代码

这个块正常退出或存在一个异常时,都会调用in.close()方法,就好像使用了finally块同样。

还能够指定多个资源:

try(Scanner in = new Scanner(new FileInputStream("..."), "UTF-8");
  PrintWriter out = new PrintWriter("..."))
{
  while(in.hasNext())
    System.out.println(in.next().toUpperCase());
}
复制代码

不论如何这个块如何退出,inout都会关闭,可是若是用常规手动编程,就须要两个嵌套的try/finally语句。

以前的close抛出异常会带来难题,而带资源的try语句能够很好的处理这种状况,原来的异常会被从新抛出,而close方法带来的异常会“被抑制”。

分析堆栈轨迹元素

堆栈轨迹(stack trace)是一个方法调用过程的列表,包含了程序执行过程当中方法调用的特定位置。

能够调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。

Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
复制代码

一种更灵活的方法是使用getStackTrace方法,会获得StackTraceElement对象的一个数组,能够在程序中分析这个对象数组:

StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame : frames)
  analyze frame
复制代码

StackTraceElement类含有可以得到文件名和当前执行的代码行号的方法,同时还含有能得到类名和方法名的方法,toString方法会产生一个格式化的字符串,其中包含所得到的信息。

静态的Thread.getAllStackTraces方法,能够产生全部线程的堆栈轨迹。

Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for(Thread t : map.keySet())
{
  StackTraceElememt[] frames = map.get(t);
  analyze frames
}
复制代码

java.lang.Throwable

  • Throwable(Throwable cause)
  • Throwable(String message, Throwable cause)
  • Throwable initCause(Throwable cause):将这个对象设置为“缘由”,若是这个对象已经被设置为“缘由”,则抛出一个异常,返回this引用。
  • Throwable getCause():得到设置为这个对象的“缘由”的异常对象,若是没有则为null
  • StackTraceElement[] getStackTrace():得到构造这个对象时调用堆栈的跟踪
  • void addSuppressed(Throwable t):为这个异常增长一个抑制异常
  • Throwable[] getSuppressed():获得这个异常的全部抑制异常

java.lang.StackTraceElement

  • String getFileName()
  • int getLineNumber()
  • String getClassName()
  • String getMethodName()
  • boolean isNativeMethod():若是这个元素运行时在一个本地方法中,则返回true
  • String toString():若是存在的话,返回一个包含类名、方法名、文件名和行数的格式化字符串,如StackTraceTest.factorial(StackTraceTest.java:18)

使用异常机制的技巧

1.异常处理不能代替简单的测试

在进行一些风险操做时(好比出栈操做),应该先检测当前操做是否有风险(好比检查是否已经空栈),而不是用异常捕获来代替这个测试。

与简单的测试相比,捕获异常须要花费更多的时间,因此:只在异常状况下使用异常机制

2.不要过度细分化异常

若是能够写成一个try/catch(s)的语句,那就不要写成多个try/catch

3.利用异常层次结构

不要只抛出RuntimeException异常,应该寻找更适合的子类或建立本身的异常类。

不要只抛出Throwable异常,不然会使程序代码可读性、可维护性降低。

4.不要压制异常

在Java中,倾向于关闭异常。

public Image loadImage(String s) {
  try
  {
    codes
  }
  catch(Exception e)
  {}
}
复制代码

这样代码就能够经过编译了,若是发生了异常就会被忽略。固然若是认为异常很是重要,就应该对它们进行处理。

5.检测错误时,“苛刻”要比听任更好

6.不要羞于传递异常

有时候传递异常比捕获异常更好,让高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜。

断言

这部分和测试相关,之后有须要的话单独开设一章进行说明。

记录日志

不要再使用System.out.println来进行记录了

使用记录日志API吧

基本日志

简单的日志记录,可使用全局日志记录器(global logger)并调用info方法:

Logger.getGlobal().info("File->Open menu item selected");
复制代码

默认状况下会显示:

May 10, 2013 10:12:15 ....
INFO: File->Open menu item selected
复制代码

若是在适当的地方调用:

Logger.getGlobal().setLevel(Level.OFF);
复制代码

高级日志

能够不用将全部的日志都记录到一个全局日志记录器中,也能够自定义日志记录器:

private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
复制代码

未被任何变量引用的日志记录器可能会被垃圾回收,为了不这种状况,能够用一个静态变量存储日志记录器的一个引用。

与包名相似,日志记录器名也具备层次结构,而且层次性更强。

对于包来讲,包的名字与其父包没有语义关系,可是日志记录器的父与子之间共享某些属性。

例如,若是对com.mycompany日志记录器设置了日志级别,它的子记录器也会继承这个级别。

一般有如下7个日志记录器级别Level

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

默认状况下,只记录前三个级别。

另外,可使用Level.ALL开启全部级别的记录,或者使用Level.OFF关闭全部级别的记录。

对于全部的级别有下面几种记录方法:

logger.warning(message);
logger.info(message);
复制代码

也可使用log方法指定级别:

logger.log(Level.FINE, message);
复制代码

若是记录为INFO或更低,默认日志处理器不会处理低于INFO级别的信息,能够经过修改日志处理器的配置来改变这一情况。

默认的日志记录将显示包含日志调用的类名和方法名,如同堆栈所显示的那样。

可是若是虚拟机对执行过程进行了优化,就得不到准确的调用信息,此时,能够调用logp方法得到调用类和方法的确切位置,这个方法的签名为:

void logp(Level l, String className, String methodName, String message) 复制代码

记录日志的常见用途是记录那些不可预料的异常,可使用下面两个方法提供日志记录中包含的异常描述内容:

if(...)
{
  IOException exception = new IOException("...");
  logger.throwing("com.mycompany.mylib.Reader", "read", exception);
  throw exception;
}
复制代码

还有

try
{
  ...
}
catch(IOException e)
{
  Logger.getLogger("com.mycompany.myapp").log(Level.WARNING, "Reading image", e);
  z
}
复制代码

调用throwing能够记录一条FINER级别的记录和一条以THROW开始的信息。

剩余部分暂时不作介绍,初步了解到这便可,一把要结合IDE一块儿来使用这个功能。若是后续的高级知识部分有须要的话会单独开设专题来介绍。

Java异常、断言和日志总结

  • 处理错误
  • 异常分类
  • 受查异常
  • 抛出异常
  • 建立异常类
  • 捕获异常
  • 再次抛出异常与异常链
  • finally子句
  • 在资源的try语句
  • 分析堆栈轨迹元素
  • 使用异常机制的技巧
  • 基本日志与高级日志

我的静态博客:

相关文章
相关标签/搜索