捕获异常
捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台 上打印出异常信息, 其中包括异常的类型和堆栈的内容。
要想捕获一个异常, 必须设置 try/catch语句块。最简单的 try语句块如下所示:
try{ code more code more code } catch (ExceptionType e) { handlerfor this type }
如果在 try语句块中的任何代码抛出了一个在 catch子句中说明的异常类,那么
1 ) 程序将跳过 try语句块的其余代码。
2 ) 程序将执行 catch 子句中的处理器代码。
如果在 try 语句块中的代码没有拋出任何异常,那么程序将跳过 catch 子句。
如果方法中的任何代码拋出了一个在 catch 子句中没有声明的异常类型,那么这个方法 就会立刻退出(希望调用者为这种类型的异常设tKT catch 子句)。
如果想传递一个异常, 就必须在方法的首部添加一个 throws说明符, 以便告知调用者这 个方法可能会抛出异常。
仔细阅读一下 Java API 文档, 以便知道每个方法可能会抛出哪种异常, 然后再决定是自己处理,还是添加到 throws 列表中。对于后一种情况,也不必犹豫。将异常直接交给能够胜 任的处理器进行处理要比压制对它的处理更好。
如果编写一个覆盖超类的方法, 而这个方法又没有抛出异常(如 JComponent 中的 paintComponent), 那么这个方法就必须捕 获方法代码中出现的每一个受查异常。不允许在子类的 throws说明符中出现超过超类方法所 列出的异常类范围。
捕获多个异常
在一个 try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可 以按照下列方式为每个异常类型使用一个单独的 catch 子句:
try { code that might throwexceptions } catch (FileNotFoundException e) { emergencyactionfor missingfiles } catch (UnknownHostException e) { emergencyactionfor unknown hosts } catch (IOException e) { emergencyactionforallother I/Oproblems }
异常对象可能包含与异常本身有关的信息。要想获得对象的更多信息, 可以试着使用
e.getHessage()
得到详细的错误信息(如果有的话),或者使用
e.getClass().getName()
得到异常对象的实际类型。
在 Java SE 7中,同一个 catch 子句中可以捕获多个异常类型。例如,假设对应缺少文件 和未知主机异常的动作是一样的,就可以合并 catch 子句:
try { codethat might throwexceptions } catch (FileNotFoundException | UnknownHostException e) { emergencyactionfor missing filesand unknown hosts } catch (IOException e) { emergencyactionforall other I/O problems }
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。
再次抛出异常与异常链
在 catch 子句中可以抛出一个异常,这样做的目的是改变异常的类型。 如果开发了一个 供其他程序员使用的子系统, 那么,用于表示子系统故障的异常类型可能会产生多种解释。 ServletException 就是这样一个异常类型的例子。
try { access thedatabase } catch (SQLException e) { throw new ServletException("database error: " + e.getMessageO); }
这里,ServleException 用带有异常信息文本的构造器来构造。
不过,可以有一种更好的处理方法,并且将原始异常设置为新异常的“ 原因”:
try { accessthedatabase }catch (SQLException e) { Throwable se = new ServletException("database error"); se.initCause(e); throw se; }
当捕获到异常时, 就可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause();
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异 常的细节。
finally 子句
不管是否有异常被捕获,finally 子句中的代码都被执行。在下面的示例中, 程序将在所 有情况下关闭文件。
InputStream in = new FileInputStream(. ..); try{ //1 code that might throwexceptions //2 } catch (IOException e) { // 3 showerror message // 4 } finally { // 5 in.close(); } //6
在上面这段代码中,有下列 3 种情况会执行 finally 子句:
1 ) 代码没有抛出异常。这种情况下只执行标注的1、2、5、6。
2 ) 抛出一个在 catch 子句中捕获的异常。在上面的示例中就是 IOException 异常。这种情况下只执行标注的1、3、4、5、6。
3 ) 代码抛出了一个异常, 但这个异常不是由 catch 子句捕获的。在这种情况下只执行标注的1、5。
这里, 强烈建议解搞合 try/catch 和 try/finally 语句块。这样可以提高代码的清晰 度。例如:
InputStrean in = . . .; try { try { code that might throwexceptions } finally { in.close(); } } catch (IOException e) { showerrormessage }
内层的 try语句块只有一个职责, 就是确保关闭输入流。外层的 try语句块也只有一个职 责, 就是确保报告出现的错误。这种设计方式不仅清楚, 而且还具有一个功能,就是将 会报告 finally 子句中出现的错误。
带资源的 try语句
带资源的 try语句(try-with-resources) 的最简形式为:
try (Resource res = . . .) { work with res }
try块退出时,会自动调用 res.dose()。下面给出一个典型的例子, 这里要读取一个文件 中的所有单词:
try (Scanner in = new Scanner(new FileInputStream("7usr/share/dict/words")), "UTF-8") { while (in.hasNext()) System.out.println(in.next()); }
这个块正常退出时, 或者存在一个异常时, 都会调用 in.close()方法, 就好像使用了 finally块一样。
还可以指定多个资源: 例如:
try (Scanner in = new Scanner(new FileInputStream("7usr/share/dict/words")."UTF-8"): PrintWriter out = new PrintWriter("out.txt")) { while (in.hasNextO) out.println(in.next().toUpperCaseO); }
不论这个块如何退出, in 和 out 都会关闭。如果你用常规方式手动编程,就需要两个嵌 套的 try/finally语句。
分析堆栈轨迹元素
堆栈轨迹(stack trace) 是一个方法调用过程的列表, 它包含了程序执行过程中方法调用 的特定位置。
可以调用 Throwable 类的 printStackTrace 方法访问堆栈轨迹的文本描述信息
Throwable t = new Throwable(); StringWriter out = new StringWriter(); t.printStackTrace(new PrintWriter(out)); String description = out.toString();
一种更灵活的方法是使用 getStackTrace 方法, 它会得到 StackTraceElement 对象的一个 数组, 可以在你的程序中分析这个对象数组。例如:
Throwable t = new ThrowableO; StackTraceElement[] frames = t.getStackTrace(); for (StackTraceElement frame : frames) analyzeframe
StackTraceElement 类含有能够获得文件名和当前执行的代码行号的方法, N时, 还含有 能够获得类名和方法名的方法。toString 方法将产生一个格式化的字符串, 其屮包含所获得 的信息。
静态的 Thread.getAllStackTrace 方法, 它可以产生所有线程的堆栈轨迹 . 下面给出使用 这个方法的具体方式:
Map<Thread, StackTraceElement[]> map = Thread.getAl1StackTraces(); for (Thread t : map.keySet()) { StackTraceElement[] frames = map.get(t); analyze frames }
程序清单 7-1 打印了递归阶乘函数的堆栈情况例如, 如果计算 factorials), 将会打印 下列内容:
factorial(3): StackTraceTest.factorial(StackTraceTest.java:18) StackTraceTest.main(StackTraceTest.java:34) factorial(2): StackTraceTest.factorial(StackTraceTest.java:18) StackTraceTest.factorial(StackTraceTest.java:24) StackTraceTest.main(StackTraceTest.java:34) factorial(1): StackTraceTest.factorial(StackTraceTest.java:18) StackTraceTest.factorial(StackTraceTest.java:24) StackTraceTest.factorial(StackTraceTest.java:24) StackTraceTest.main(StackTraceTest ava:34) return 1 return 2 return 6
//程序清单 7-1 stackTrace/StackTraceTest.java package stackTrace; import java.util.*; /** * A program that displays a trace feature of a recursive method call. * @version 1.01 2004-05-10 * @author Cay Horstmann */ public class StackTraceTest { /** * Computes the factorial of a number * @param n a non-negative integer * @return n! = 1 * 2 * . . . * n */ public static int factorial(int n) { System.out.println("factorial(" + n + "):"); Throwable t = new Throwable(); StackTraceElement[] frames = t.getStackTrace(); for (StackTraceElement f : frames) System.out.println(f); int r; if (n <= 1) r = 1; else r = n * factorial(n - 1); System.out.println("return " + r); return r; } public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.print("Enter n: "); int n = in.nextInt(); factorial(n); } }
使用异常机制的技巧
下面给出使用异常机制的几个技巧。
-
异常处理不能代替简单的测试。
-
不要过分地细化异常。
-
利用异常层次结构。
-
不要压制异常 。
-
在检测错误时, “ 苛刻” 要比放任更好 。
-
不要羞于传递异常。
使用断言
断言的概念
假设确信某个属性符合要求,并且代码的执行依赖于这个属性。例如, 需要计算
double y = Math.sqrt(x);
我们确信,这里的 X 是一个非负数值。原因是:X 是另外一个计算的结果,而这个结果 不可能是负值;或者 X 是一个方法的参数,而这个方法要求它的调用者只能提供一个正整数。 然而,还是希望进行检查, 以避免让“ 不是一个数” 的数值参与计算操作。当然,也可以抛 出一个异常:
if (x < 0) throw new IllegalArgumentException("x < 0");
但是这段代码会一直保留在程序中, 即使测试完毕也不会自动地删除。如果在程序中含 有大量的这种检查,程序运行起来会相当慢。
断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插人的检测语句将会被自动地移走。
Java语言引人了关键字 assert。这个关键字有两种形式:
assert 条件;
和 assert 条件:表达式;
这两种形式都会对条件进行检测, 如果结果为 false, 则抛出一个 AssertionError 异常。 在第二种形式中,表达式将被传人 AssertionError 的构造器,并转换成一个消息字符串。
要想断言?c 是一个非负数值, 只需要简单地使用下面这条语句
assert x >= 0;
或者将 x 的实际值传递给 AssertionError 对象, 从而可以在后面显示出来。
assert x >= 0 : x;
启用和禁用断言
在默认情况下,断言被禁用。可以在运行程序时用 -enableassertions 或 -ea选项启用:
java -enableassertions MyApp
需要注意的是, 在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器 ( class loader) 的功能。当断言被禁用时, 类加载器将跳过断言代码, 因此,不会降低程序运 行的速度。
也可以在某个类或整个包中使用断言, 例如:
java -ea:MyClass -eaiconi.inycompany.inylib.., MyApp
这条命令将开启 MyClass类以及在 com.mycompany.mylib 包和它的子包中的所有类的断 言。选项 -ea 将开启默认包中的所有类的断言。
也可以用选项-disableassertions 或 -da 禁用某个特定类和包的断言:
java -ea:... -da:MyClass MyApp
有些类不是由类加载器加载, 而是直接由虚拟机加载。可以使用这些开关有选择地启用 或禁用那些类中的断言。
然而, 启用和禁用所有断言的 -ea 和 -da 开关不能应用到那些没有类加载器的“ 系统类” 上。对于这些系统类来说, 需要使用-enablesystemassertions/-esa 开关启用断言。
使用断言完成参数检查
在 Java 语言中, 给出了 3 种处理系统错误的机制:
•抛出一个异常
•日志
•使用断言
什么时候应该选择使用断言呢? 请记住下面几点:
•断言失败是致命的、 不可恢复的错误。
•断言检查只用于开发和测阶段
记录曰志
基本曰志
要生成简单的日志记录,可以使用全局日志记录器(global logger) 并调用其 info方法:
Logger.getClobal0,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);
将会取消所有的日志。
高级曰志
在一 个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义 日志记录器。
可以调用 getLogger方法创建或获取记录器:
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
通常, 有以下 7 个日志记录器级别:
• SEVERE
• WARNING
• INFO
• CONFIG
• FINE
• FINER
• FINEST
在默认情况下,只记录前三个级别。 也可以设置其他的级別。例如,
logger,setLevel(Level.FINE);
现在, FINE 和更高级别的记录都可以记录下来。 另外, 还可以使用 Level.ALL 开启所有级别的记录, 或者使用 Level.OFF 关闭所有级别 的记录。 对于所有的级别有下面几种记录方法:
logger.warning(message): logger,fine(message);
同时,还可以使用 log方法指定级别, 例如:
logger.log(Level.FINE, message);
修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于:
jre/lib/1ogging.properties
要想使用另一个配置文件, 就要将 java.utiUogging.config.file 特性设置为配置文件的存 储位置, 并用下列命令启动应用程序:
java -Djava.util.logging.config.file-configFileMainClass
要想修改默认的日志记录级别, 就需要编辑配置文件,并修改以下命令行
.level=INFO
可以通过添加以下内容来指定自己的日志记录级别
com.mycompany_myapp.level=FINE
也就是说,在日志记录器名后面添加后缀 .level。
另外,处 理器也有级别。要想在控制台上看到 FINE级别的消息, 就需要进行下列设置
java.util.logging.ConsoleHandler.level=FINE
本地化
本地化的应用程序包含资源包(resource bundle) 中的本地特定信息。资源包由各个地区 (如美国或德国)的映射集合组成。例如, 某个资源包可能将字符串“ readingFile” 映射成英 文的 “ Reading file” 或者德文的“ Achtung! Datei wird eingelesen”。
一个程序可以包含多个资源包, 一个用于菜单;其他用于日志消息。每个资源包都有一个名字(如 com.mycompany.logmessages)。要想将映射添加到一个资源包中,需要为每个地 区创建一个文件。英文消息映射位于 com/mycompany/logmessages_en.properties 文件中; 德 文消息映射位于 com/mycompany/logmessages_de.properties 文件中。(en 和 de 是语言编码)。
处理器
在默认情况下t 日志记录器将记录发送到 ConsoleHandler 中, 并由它输出到 System.err 流中。特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器(命名为“ ”)有 一个 ConsoleHandler。
与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日 志记录级别必须高于日志记录器和处理器的阈值。日志管理器配置文件设置的默认控制台处 理器的日志记录级别为
java.uti1.1ogging.ConsoleHandler.level=INF0
要想记录 FINE 级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别。 另外,还可以绕过配置文件,安装自己的处理器。
Logger logger = Logger.getLogger("com.mycompany.myapp");
logger.setLevel(Level.FINE);
logger .setUseParentHandlers(false);
Handler handler = new ConsoleHandler();
handler,setLevel(Level.FINE);
logger.addHandler(hand er):
在默认情况下, 日志记录器将记录发送到自己的处理器和父处理器。我们的日志记录 器是原始日志记录器(命名为“ ”)的子类, 而原始日志记录器将会把所有等于或高于 INFO级別的记录发送到控制台。然而, 我们并不想两次看到这些记录。鉴于这个原因,应该将 useParentHandlers M性设置为 false。
可以通过设置 H 志管理器配置文件中的不同参数(请参看表 7-1 ), 或者利用其他的构造 器(请参看本节后面给出的 APf 注释)来修改文件处理器的默认行为。

也冇可能不想使用默认的日志记录文件名, 因此, 应该使用另一种模式, 例如, %h/ myapp.log ( 有关模式变量的解释请参看表 7-2 )。

如果多个应用程序(或者同一个应用程序的多个副本)使用同一个口志文件, 就应该开 启 append 标志。另外, 应该在文件名模式中使用 %u, 以便每个应用程序创建日志的唯一 副本。
过滤器
在默认情况下, 过滤器根据日志记录的级别进行过滤。每个日志记录器和处理器都可以 有一个可选的过滤器来完成附加的过滤。另外,可以通过实现 niter 接口并定义下列方法来 自定义过滤器。
boolean isLoggab1e(LogRecord record)
在这个方法中,可以利用自己喜欢的标准,对日志记录进行分析,返回 true 表示这些记 录应该包含在日志中。
要想将一个过滤器安装到一个日志记录器或处理器中,只需要调用 setFilter方法就可以 了。注意,同一时刻最多只能有一个过滤器。
格式化器
ConsoleHandler类和 FileHandler类可以生成文本和 XML 格式的日志记录。但是, 也可 以自定义格式。这需要扩展 Formatter 类并覆盖下面这个方法:
String format(LogRecord record)
可以根据自己的愿望对记录中的信息进行格式化,并返冋结果字符串。在 format方法 中, 有可能会调用下面这个方法
String formatMessage(LogRecord record)
这个方法对记录中的部分消息进行格式化、 参数替换和本地化应用操作。
很多文件格式(如 XML) 需要在已格式化的记录的前后加上一个头部和尾部。
日志记录说明
下面的“ 日志说明书”总结了一些最常用的操作。
1 ) 为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程 序包一样的名字,例如,com.mycompany.myprog, 这是一种好的编程习惯。 另外,可以通过 调用下列方法得到日志记录器。
Logger logger = Logger.getLogger("com.mycompany.myprog");
2 ) 默认的日志配置将级别等于或高于 INFO级别的所有消息记录到控制台。用户可以覆 盖默认的配置文件。但是正如前面所述,改变配置需要做相当多的工作。因此,最好在应用 程序中安装一个更加适宜的默认配置。
3 ) 现在,可以记录自己想要的内容了。但需要牢记: 所有级别为 INFO、 WARNING 和 SEVERE 的消息都将显示到控制台上。 因此, 最好只将对程序用户有意义的消息设置为这几 个级别。将程序员想要的日志记录,设定为 FINE 是一个很好的选择。
程序清单 7-2 利用上述说明可实现:日志记录消息也显示在日志窗口中。
//程序清单 7-2 logging/LogginglmageViewer.java package logging; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.logging.*; import javax.swing.*; /** * A modification of the image viewer program that logs various events. * @version 1.02 2007-05-31 * @author Cay Horstmann */ public class LoggingImageViewer { public static void main(String[] args) { if (System.getProperty("java.util.logging.config.class") == null && System.getProperty("java.util.logging.config.file") == null) { try { Logger.getLogger("com.horstmann.corejava").setLevel(Level.ALL); final int LOG_ROTATION_COUNT = 10; Handler handler = new FileHandler("%h/LoggingImageViewer.log", 0, LOG_ROTATION_COUNT); Logger.getLogger("com.horstmann.corejava").addHandler(handler); } catch (IOException e) { Logger.getLogger("com.horstmann.corejava").log(Level.SEVERE, "Can't create log file handler", e); } } EventQueue.invokeLater(new Runnable() { public void run() { Handler windowHandler = new WindowHandler(); windowHandler.setLevel(Level.ALL); Logger.getLogger("com.horstmann.corejava").addHandler(windowHandler); JFrame frame = new ImageViewerFrame(); frame.setTitle("LoggingImageViewer"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Logger.getLogger("com.horstmann.corejava").fine("Showing frame"); frame.setVisible(true); } }); } } /** * The frame that shows the image. */ class ImageViewerFrame extends JFrame { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 400; private JLabel label; private static Logger logger = Logger.getLogger("com.horstmann.corejava"); public ImageViewerFrame() { logger.entering("ImageViewerFrame", "<init>"); setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); // set up menu bar JMenuBar menuBar = new JMenuBar(); setJMenuBar(menuBar); JMenu menu = new JMenu("File"); menuBar.add(menu); JMenuItem openItem = new JMenuItem("Open"); menu.add(openItem); openItem.addActionListener(new FileOpenListener()); JMenuItem exitItem = new JMenuItem("Exit"); menu.add(exitItem); exitItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { logger.fine("Exiting."); System.exit(0); } }); // use a label to display the images label = new JLabel(); add(label); logger.exiting("ImageViewerFrame", "<init>"); } private class FileOpenListener implements ActionListener { public void actionPerformed(ActionEvent event) { logger.entering("ImageViewerFrame.FileOpenListener", "actionPerformed", event); // set up file chooser JFileChooser chooser = new JFileChooser(); chooser.setCurrentDirectory(new File(".")); // accept all files ending with .gif chooser.setFileFilter(new javax.swing.filechooser.FileFilter() { public boolean accept(File f) { return f.getName().toLowerCase().endsWith(".gif") || f.isDirectory(); } public String getDescription() { return "GIF Images"; } }); // show file chooser dialog int r = chooser.showOpenDialog(ImageViewerFrame.this); // if image file accepted, set it as icon of the label if (r == JFileChooser.APPROVE_OPTION) { String name = chooser.getSelectedFile().getPath(); logger.log(Level.FINE, "Reading file {0}", name); label.setIcon(new ImageIcon(name)); } else logger.fine("File open dialog canceled."); logger.exiting("ImageViewerFrame.FileOpenListener", "actionPerformed"); } } } /** * A handler for displaying log records in a window. */ class WindowHandler extends StreamHandler { private JFrame frame; public WindowHandler() { frame = new JFrame(); final JTextArea output = new JTextArea(); output.setEditable(false); frame.setSize(200, 200); frame.add(new JScrollPane(output)); frame.setFocusableWindowState(false); frame.setVisible(true); setOutputStream(new OutputStream() { public void write(int b) { } // not called public void write(byte[] b, int off, int len) { output.append(new String(b, off, len)); } }); } public void publish(LogRecord record) { if (!frame.isVisible()) return; super.publish(record); flush(); } }