七、异常,断言和日志

一、处理错误

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

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

异常分类

  

   Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误。 应用程序不应该 抛出这种类型的对象。 如果出现了这样的内部错误, 除了通告给用户,并尽力使程序安全地 终止之外, 再也无能为力了。这种情况很少出现。:由 程序错误导致的异常属于 RuntimeException ; 而程序本身没有问题, 但由于像 I/O 错误这类 问题导致的异常属于其他异常:

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

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

不是派生于 RuntimeException 的异常包括:

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

   Java 语 言 规 范 将 派 生 于 Error 类 或 RuntimeException 类的所有异常称为非受查 ( unchecked ) 异常,所有其他的异常称为受查( checked) 异常。这是两个很有用的术语,在 后面还会用到。 编译器将核查是否为所有的受査异常提供了异常处理器。

声明受查异常

public FileInputStream(String name) throws FileNotFoundException

   这个声明表明这个构造器将根据给定的 String 参数产生一个 FilelnputStream 对象,但也 有可能抛出一个 FileNotFoimdException 异常。如果发生了这种糟糕情况, 构造器将不会初始 化一个新的 FileInputStream 对象, 而是抛出一个 FileNotFoundException 类对象。 如果这个方 法真的抛出了这样一个异常对象,运行时系统就会开始搜索异常处理器, 以便知道如何处理 FileNotFoundException 对象„

 需要记住在遇到下面 4 种 情况时应该抛出异常:

1 ) 调用一个抛出受査异常的方法, 例如, FilelnputStream 构造器。

2 ) 程序运行过程中发现错误, 并且利用 throw语句抛出一个受查异常(下一节将详细地 介绍 throw 语句)。

3 ) 程序出现错误, 例如,a[-l] =0 会抛出一个 ArraylndexOutOfBoundsException 这样的 非受查异常。

4 ) Java 虚拟机和运行时库出现的内部错误。

如果出现前两种情况之一, 则必须告诉调用这个方法的程序员有可能抛出异常。

  对于那些可能被他人使用的 Java 方法, 应该根据异常规范( exception specification), 在 方法的首部声明这个方法可能抛出的异常。如果一个方法有可能抛出多个受查异常类型, 那么就必须在方法的首部列出所有的异常 类。每个异常类之间用逗号隔开。但是, 不需要声明 Java 的内部错误,即从 Error 继承的错误。任何程序代码都具有抛出那些 异常的潜能, 而我们对其没有任何控制能力。 同样,也不应该声明从 RuntimeException 继承的那些非受查异常。

  总之,一个方法必须声明所有可能抛出的受查异常, 而非受查异常要么不可控制( Error), 要么就应该避免发生( RuntimeException)。如果方法没有声明所有可能发生的受查异常, 编 译器就会发出一个错误消息。

   如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方 法中声明的异常更通用 (也就是说, 子类方法中可以抛出更特定的异常, 或者根本不抛 出任何异常)。特别需要说明的是, 如果超类方法没有抛出任何受查异常, 子类也不能抛 出任何受查异常。

  如果类中的一个方法声明将会抛出一个异常, 而这个异常是某个特定类的实例时, 则这个方法就有可能抛出一个这个类的异常, 或者这个类的任意一个子类的异常。

  在 Java 中, 没有 throws 说明符的方法将不能抛出任何受查异常。

如何抛出异常

例:

String readData(Scanner in) throws EOFException{
		while() {
			if(!in.next) {
				if(n<len) {
					throw new EOFException();
				}
			}
		}
	}

对于一个已经存在的异常类, 将其抛出非常容易. 在这种情况下:

1 ) 找到一个合适的异常类。

2 ) 创建这个类的一个对象。

3 ) 将对象抛出。

 一旦方法抛出了异常, 这个方法就不可能返回到调用者。也就是说, 不必为返回的默认 值或错误代码担忧。

创建异常类

  我们需要做的只是定义一个派生于 Exception 的类,或者派生于 Exception 子类的类。定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息 的构造器(超类 Throwable 的 toString 方法将会打印出这些详细信息, 这在调试中非常有用)。

class F extends IOException{
	public F() {};
	public F(String gripe) {
		super(gripe);
	}
}

 

二、捕获异常

捕获异常

  如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台 上打印出异常信息, 其中包括异常的类型和堆栈的内容。

  要想捕获一个异常, 必须设置 try/catch语句块。最简单的 try语句块如下所示:

try{
more code
}
catch (ExceptionType e)
{
handlerfor this type
}

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

1 ) 程序将跳过 try语句块的其余代码。

2 ) 程序将执行 catch 子句中的处理器代码。

如果在 try 语句块中的代码没有拋出任何异常,那么程序将跳过 catch 子句。

  如果调用了一个抛出受查异常的方法,就必 须对它进行处理, 或者继续传递。

  如果编写一个覆盖超类的方法, 而这个方法又没有抛出异常(如 JComponent 中的 paintComponent ), 那么这个方法就必须捕 获方法代码中出现的每一个受查异常。不允许在子类的 throws 说明符中出现超过超类方法所 列出的异常类范围。

捕获多个异常

  异常对象可能包含与异常本身有关的信息。要想获得对象的更多信息, 可以试着使用 e.getMessage() 得到详细的错误信息(如果有的话,) 或者使用 e.getClass().getName()得到异常对象的实际类型。

  同一个 catch 子句中可以捕获多个异常类型。捕获多个异常时, 异常变量隐含为 final 变量。例如,不能在以下子句体中为 e 赋 不同的值:

catch (FileNotFoundException | UnknownHostException e) { . . . }

例:

try
{
code that might throw exceptions
}
catch (FileNotFoundException | UnknownHostException e)
{
emergency action for missing files and unknown hosts
}
catch (IOException e)
{
emergency action for all other I/O problems
}

再次抛出异常与异常链

  在 catch 子句中可以抛出一个异常,这样做的目的是改变异常的类型。不过,可以有一种更好的处理方法,并且将原始异常设置为新异常的“ 原因”:

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

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

Throwable e=se.getCause();

   强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

  有时你可能只想记录一个异常, 再将它重新抛出, 而不做任何改变:

try
{
access the database
}
catch (Exception e)
{
logger.log(level, message, e);
throw e;
}

  假设 e 在 catch 块中未改变, 将外围方法声明为 throws SQLException 就是合法的。

finally子句

  当代码抛出一个异常时, 就会终止方法中剩余代码的处理,并退出这个方法的执行。如 果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前 必须被回收,那么就会产生资源回收问题。一种解决方案是捕获并重新抛出所有的异常。但 是,这种解决方案比较乏味,这是因为需要在两个地方清除所分配的资源。一个在正常的代 码中;另一个在异常代码中。Java 有一种更好的解决方案,这就是 finally 子句。

  不管是否有异常被捕获,finally 子句中的代码都被执行。在下面的示例中, 程序将在所 有情况下关闭文件。

InputStream in = new FileInputStream(. . .);
try{
I I I
code that might throwexceptions
I I I
}
catch (IOException e)
{
// 3
showerror message
// 4
}
finally
{
// 5
in.closeO;
}
//6

  在上面这段代码中,有下列 3 种情况会执行 finally 子句:

1 ) 代码没有抛出异常。 在这种情况下, 程序首先执行 try 语句块中的全部代码,然后执 行 finally 子句中的代码t 随后, 继续执行 try 语句块之后的第一条语句。也就是说,执行标 注的 1、 2、 5、 6 处。

2 ) 抛出一个在 catch 子句中捕获的异常。在上面的示例中就是 IOException 异常。在这种 情况下,程序将执行 try语句块中的所有代码,直到发生异常为止。此时,将跳过 try语句块中 的剩余代码,转去执行与该异常匹配的 catch 子句中的代码, 最后执行 finally 子句中的代码。 如果 catch 子句没有抛出异常,程序将执行 try 语句块之后的第一条语句。在这里,执行 标注 1、 3、 4、5、 6 处的语句。 如果 catch 子句抛出了一个异常, 异常将被抛回这个方法的调用者。在这里, 执行标注 1、 3、 5 处的语句。

3 ) 代码抛出了一个异常, 但这个异常不是由 catch 子句捕获的。在这种情况下,程序将 执行 try 语句块中的所有语句,直到有异常被抛出为止。此时, 将跳过 try 语句块中的剩余代 码, 然后执行 finally 子句中的语句, 并将异常抛给这个方法的调用者。在这里, 执行标注 1、 5 处的语句。

try 语句可以只有 finally 子句,而没有 catch 子句。

  强烈建议解搞合 try/catch 和 try/finally 语句块。这样可以提高代码的清晰度。例如:

InputStrean in = . . .;
try
{
try
{
code that might throwexceptions
}
finally
{
in.doseO;
}
}
catch (IOException e)
{
show error message
}

  内层的 try语句块只有一个职责, 就是确保关闭输入流。外层的 try 语句块也只有一个职 责, 就是确保报告出现的错误。这种设计方式不仅清楚, 而且还具有一个功能,就是将 会报告 finally 子句中出现的错误。

  当 finally 子句包含 return 语句时, 将会出现一种意想不到的结果„ 假设利用 return 语句从 try语句块中退出。在方法返回前, finally 子句的内容将被执行。如果 finally 子句中 也有一个 return 语句,这个返回值将会覆盖原始的返回值。

  有时候, finally 子句也会带来麻烦。例如, 清理资源的方法也有可能抛出异常。现在,假设在 try 语句块中的代码抛出了一些非 IOException 的异常,这些异常只有这个 方法的调用者才能够给予处理。执行 finally 语句块,并调用 dose 方法。而 close 方法本身也 有可能抛出 IOException 异常。当出现这种情况时, 原始的异常将会丢失,转而抛出 close 方 法的异常。 这会有问题, 因为第一个异常很可能更有意思。如果你想做适当的处理,重新抛出原来 的异常, 代码会变得极其繁琐:

InputStream in = . . .;
Exception ex = null;
try{
try{
code that might throw exceptions
}
catch (Exception e){
ex=e;
throw e;
}
}
finally
{
try
{
in.closeO;
}
catch (Exception e)
{
if (ex = null) throw e;
}
}

带资源的try语句

  带资源的 try 语句(try-with-resources) 的最简形式为:

try (Resource res = . . .)
{
work with res
}

try块退出时,会自动调用 res.close()。下面给出一个典型的例子, 这里要读取一个文件 中的所有单词:

try (Scanner in = new Scanner(new FileInputStream(/usr/share/dict/words")), "UTF-8")
{
while (in.hasNext())
System.out.println(in.next());
}

这个块正常退出时, 或者存在一个异常时, 都会调用 inxloseO 方法, 就好像使用了 finally块一样。 还可以指定多个资源: 例如:

try (Scanner in = new Scanner(new FileInputStream('/usr/share/dict/words"). "UTF-8"):
PrintWriter out = new PrintWriter("out.txt"))
{
while (in.hasNext())
out.println(in.next().toUpperCaseO);
}

分析堆栈轨迹元素

  堆栈轨迹( stack trace ) 是一个方法调用过程的列表, 它包含了程序执行过程中方法调用 的特定位置前面已经看到过这种列表, 当 Java 程序正常终止, 而没有捕获异常时, 这个列表就会显示出来。

  可以调用 Throwable 类的 printStackTrace 方法访问堆栈轨迹的文本描述信息.

Throwable t = new ThrowableO;
StringWriter out = new StringWri ter() ;
t.printStackTrace(new PrintWriter(out));
String description = out.toString();

一种更灵活的方法是使用 getStackTrace 方法, 它会得到 StackTraceElement 对象的一个 数组, 可以在你的程序中分析这个对象数组。

Throwable t = new Throwable() ;
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames){
analyze frame
}

静态的 Thread.getAllStackTrace 方法, 它可以产生所有线程的堆栈轨迹 .

Map<Thread, StackTraceElement[]> map = Thread.getAl1StackTraces();
for (Thread t : map. keySet ())
{
StackTraceElement[] frames = map.get(t);
analyze frames
}

例子:

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);
   }
}

三、使用异常机制的技巧

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

  2. 不要过分地细化异常

  3. 利用异常层次结构

  4. 不要压制异常

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

  6. 不要羞于传递异常

四、使用断言

  设确信某个属性符合要求, 并且代码的执行依赖于这个属性。断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插人的检测 语句将会被自动地移走。

   Java 语言引人了关键字 assert。这个关键字有两种形式:

  assert 条件;和

  assert 条件:表达式;

  这两种形式都会对条件进行检测, 如果结果为 false, 则抛出一个 AssertionError 异常。 在第二种形式中,表达式将被传人 AssertionError 的构造器, 并转换成一个消息字符串。“ 表达式” 部分的唯一目的是产生一个消息字符串。

启用或禁用断言

1.Run -> Run Configurations -> Arguments页签 -> VM arguments文本框中加上断言开启的标志:-enableassertions 或者-ea 就可以了

  

使用断言完成参数检查

在 Java 语言中, 给出了 3 种处理系统错误的机制:

•抛出一个异常

•日志

•使用断言

什么时候应该选择使用断言呢? 请记住下面几点:

•断言失败是致命的、 不可恢复的错误。

•断言检查只用于开发和测阶段(这种做法有时候被戏称为“ 在靠近海岸时穿上救生衣, 但在海中央时就把救生衣抛掉吧”)。

  因此,不应该使用断言向程序的其他部分通告发生了可恢复性的错误,或者,不应该作 为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置。

  在方法 的开头使用断言: assert a != null; 计算机科学家将这种约定称为前置条件( Precondition)。

五、记录日志

  基本曰志

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

Logger.getClobal().info("File->Open menu item selected");

但是, 如果在适当的地方(如 main 开始)调用

Logger.getClobal ().setLevel (Level .OFF);

将会取消所有的日志。

高级日志

  在一 个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义 日志记录器。 可以调用 getLogger 方法创建或获取记录器: 

private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp"):

提示:未被任何变量引用的日志记录器可能会被垃圾回收。 为了防止这种情况发生,要 像上面的例子中一样, 用一个静态变量存储日志记录器的一个引用。

  与包名类似,日志记录器名也具有层次结构。事实上, 与包名相比,日志记录器的层次 性更强。例如, 如果对 com.mycompany 日志记录器设置了日志级别, 它的子记录器也会继承这个级别 。

通常, 有以下 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);

  默认的日志记录将显示包含日志调用的类名和方法名, 如同堆栈所显示的那样。但是, 如果虚拟机对执行过程进行了优化,就得不到准确的调用信息。此时,可以调用 logp 方法获 得调用类和方法的确切位置, 这个方法的签名为:

void logp(Level 1, String className, String methodName, String message)

下面有一些用来跟踪执行流的方法:

void entering(String dassName , String methodName)

void enteringCString className , String methodName , Object param)

void entering(String className , String methodName , Object[] params)

void exiting(String className , String methodName)

void exiting(String className , String methodName , Object result)

例如:

int read(String file, String pattern)
{
logger.entering("com.mycompany.mylib.Reader", "read",
new Object[] { file, pattern });
logger.exiting("com.mycompany.mylib. Reader", "read", count):
return count ;
}

这些调用将生成 FINER 级别和以字符串 ENTRY 和 RETURN 开始的日志记录。

可以使用下面两个方法提供日志记录 中包含的异常描述内容。

void throwing(String className , String methodName , Throwable t) void log(Level 1 , String message , Throwable t)

典型的用法是:

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);
}

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

修改日志管理器配置

本地化

处理器

过滤器

格式化器

日志记录说明

六、调试技巧

1 ) 可以打印或记录任意变量的值

2)一个不太为人所知但却非常有效的技巧是在每一个类中放置一个单独的 main方法。 这样就可以对每一个类进行单元测试。

3 ) 日志代理( logging proxy) 是一个子类的对象, 它可以截获方法调用, 并进行日志记 录,然后调用超类中的方法。

4 ) 利 用 Throwable 类提供的 printStackTmce 方法,可以从任何一个异常对象中获得堆栈 情况。

  不一定要通过捕获异常来生成堆栈轨迹。只要在代码的任何位置插入下面这条语句就可 以获得堆栈轨迹: Thread.dumpStackO:

5 ) —般来说, 堆栈轨迹显示在 System.err 上。也可以利用 printStackTrace(PrintWriter s) 方法将它发送到一个文件中。

6 ) 要想观察类的加载过程, 可以用 -verbose 标志启动 Java 虚拟机。

7 ) -Xlint 选项告诉编译器对一些普遍容易出现的代码问题进行检査。例如, 如果使用 下面这条命令编译: javac -Xlint:fall through

.

猜你喜欢

转载自blog.csdn.net/qq_39326472/article/details/87641603