转载

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

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

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

处理错误

如果由于出现错误而是的某些操作没有完成,程序应该:

  • 返回到一种安全状态,并让用户执行一些其他操作;或者
  • 允许用户保存所有操作,并以妥善方式终止程序

检测(或引发)错误条件的代码通常离:

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

的代码很远。

异常处理的任务:将 控制权 从错误产生地方 转移给 能够处理这种情况的 错误处理器

异常分类

在Java中,异常对象都是派生于 Throwable 类的一个实例,如果Java中内置的异常类不能满足需求,用户还可以创建自己的异常类。

Java异常层次结构:

  • Throwable

    • Error

      • ...
    • Exception

      • IOException

        • ...
      • RuntimeException

        • ...

可以看到第二层只有 ErrorException

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

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

划分两个分支的规则是:

RuntimeException
IOException

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

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

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

Class

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

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

声明受查异常

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

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

public FileInputStream(String name) throws FileNotFoundException

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

当然不是所有方法都需要声明异常,下面4种情况应该抛出异常:

throw

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

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

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 子句中说明的异常类,那么:

try
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. 代码没有抛出异常,执行序列为1、2、5、6
  2. 抛出一个在 catch 子句中捕获的异常

    catch
    catch
    
  3. 代码抛出了一个异常,但是这个异常没有被捕获,执行序列为1、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

个人静态博客:

  • 气泡的前端日记: https://rheabubbles.github.io
原文  https://segmentfault.com/a/1190000018068571
正文到此结束
Loading...