《Java核心技术 卷Ⅰ》 第7章 异常、断言和日志
如果由于出现错误而是的某些操作没有完成,程序应该:
检测(或引发)错误条件的代码通常离:
的代码很远。
异常处理的任务:将 控制权 从错误产生地方 转移给 能够处理这种情况的 错误处理器 。
在Java中,异常对象都是派生于 Throwable
类的一个实例,如果Java中内置的异常类不能满足需求,用户还可以创建自己的异常类。
Java异常层次结构:
Throwable
Error
Exception
IOException
RuntimeException
可以看到第二层只有 Error
和 Exception
。
Error
类层次结构描述了Java运行时系统的 内部错误
和 资源耗尽错误
,应用程序不应该抛出这种类型的对象,这种内部错误的情况很少出现,出现了能做的工作也很少。
设计Java程序时,需关注 Exception
层次结构,这个层次又分为两个分支, RuntimeException
和包含其他异常的 IOException
。
划分两个分支的规则是:
RuntimeException IOException
派生于 RuntimeException
的异常包含下面几种情况:
派生于 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);
对于一个已经存在的异常类,将其抛出比较容易:
一旦抛出异常,这个方法就不可能返回到调用者,即不必为返回的默认值或错误代码担忧。
实际情况中,可能会遇到任何标准异常类不能充分描述的问题,这时候就应该创建自己的异常类。
需要做的只是定义一个 派生于
Exception
的类,或者 派生于
Exception
子类的类。
习惯上,定义的类应该包含两个构造器:
Throwable
的 toString
方法会打印出这些信息,在调试中有很多用) 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();
这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。
如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收(比如数据库连接的关闭),那么就会产生资源回收问题。
一种是捕获并重新抛出所有异常,这种需要在两个地方清除所分配的资源,一个在正常代码中,另一个在异常代码中。
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
子句:
抛出一个在 catch
子句中捕获的异常
catch catch
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中关闭资源的处理会容易很多。
对于以下代码模式:
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()); }
不论如何这个块如何退出, in
和 out
都会关闭,但是如果用常规手动编程,就需要两个嵌套的 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 }
Throwable(Throwable cause)
Throwable(String message, Throwable cause)
Throwable initCause(Throwable cause)
:将这个对象设置为“原因”,如果这个对象已经被设置为“原因”,则抛出一个异常,返回 this
引用。 Throwable getCause()
:获得设置为这个对象的“原因”的异常对象,如果没有则为 null
StackTraceElement[] getStackTrace()
:获得构造这个对象时调用堆栈的跟踪 void addSuppressed(Throwable t)
:为这个异常增加一个抑制异常 Throwable[] getSuppressed()
:得到这个异常的所有抑制异常 String getFileName()
int getLineNumber()
String getClassName()
String getMethodName()
boolean isNativeMethod()
:如果这个元素运行时在一个本地方法中,则返回 true
String toString()
:如果存在的话,返回一个包含类名、方法名、文件名和行数的格式化字符串,如 StackTraceTest.factorial(StackTraceTest.java:18)
在进行一些风险操作时(比如出栈操作),应该先检测当前操作是否有风险(比如检查是否已经空栈),而不是用异常捕获来代替这个测试。
与简单的测试相比,捕获异常需要花费更多的时间,所以: 只在异常情况下使用异常机制 。
如果可以写成一个 try/catch(s)
的语句,那就不要写成多个 try/catch
。
不要只抛出 RuntimeException
异常,应该寻找更适合的子类或创建自己的异常类。
不要只抛出 Throwable
异常,否则会使程序代码可读性、可维护性下降。
在Java中,倾向于关闭异常。
public Image loadImage(String s) { try { codes } catch(Exception e) {} }
这样代码就可以通过编译了,如果发生了异常就会被忽略。当然如果认为异常非常重要,就应该对它们进行处理。
有时候传递异常比捕获异常更好,让高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜。
这部分和测试相关,以后有需要的话单独开设一章进行说明。
不要再使用 System.out.println
来进行记录了
!
简单的日志记录,可以使用全局日志记录器(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一起来使用这个功能。如果后续的高级知识部分有需要的话会单独开设专题来介绍。
finally try
个人静态博客: