2015 7 6 Java核心技术卷一 第11章 异常 断言 日志和调试

第11章 异常 断言 日志和调试

11.1处理错误
用户期望出现错误时,程序能够采用一些理智的行为。如果由于出现错误而使得某些操作没有完成,程序应该:
--返回一种安全状态,并能够让用户执行一些其他的命令
--允许用户保存所有操作的结果,并以适当方式终止程序

异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。
为了能够在程序中处理异常状况,必须研究程序中可能会出现的错误和问题,以及哪类问题需要关注。
1.用户输入错误
2.设备错误:比如打印机被关掉了
3.物理限制:磁盘满了,可用存储空间已被用完
4.代码错误:程序方法有可能无法正确执行。例如,方法可能返回一个 错误答案,或者错误地调用了其他的方法。

在Java中如果某个方法不能够采用正常的途径完成它的任务,就可以通过另外一个路径退出方法。在这种情况下,方法并不返回任何值,而是抛出(throw)一个封装了错误信息的对象。需要注意的是,这个方法将会立刻退出,并不返回任何值。此外,调用这个方法的代码也将无法继续执行,而是,异常处理机制开始搜索能够处理这种异常状况的异常处理器。

11.1.1异常分类
在java中,异常对象都是派生于Throwable类的一个实例。稍后还可以看到,如果Java中内置的异常类不能满足要求,用户可以创建自己的异常类。
下图是Java异常层次结构的示意图

需要注意的是,所有的异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception

Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。如果出现了这样的错误,除了通告用户,并尽力使程序安全地终止之外,再也无能为力了。

在设计Java程序时,需要关注的是Exception层次结构。这个层次结构又分解为两个分支:一个分支派生于RunntimeException,另一个分支包含其他异常。划分两个分支的规则是:由程序错误导致的异常属于RunntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。

派生于RuntimeException的异常包含下面几种情况:
--错误的类型转换
--数组访问越界
--访问空指针
不是派生于RunntimeException的异常包括:
--试图在文件尾部后面读取数据
--试图打开一个不存在的文件
--试图根据给定的字符串查找Class对象,而这个字符串表示的类不存在。

“如果出现RunntimeException异常,那么就一定是你的问题”是一条相当有道理的规则。
应该通过检测数组下标是否越界避免ArrayIndexOutOfBoundsException异常;应该通过在使用变量之前检测是否为空来杜绝NullPointerException异常的发生。

Java语言规范将派生于Error类或RunntimeException类的所有异常称为未检查(unchecked)异常,
所有其他的异常称为已检查(checked)异常。

11.1.2 声明已检查异常
如果遇到了无法处理的情况,那么Java的方法可以抛出一个异常。这个道理很简单:一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。例如,一段读取文件的代码知道有可能读取的文件不存在,或者内容为空,因此,试图处理文件信息的代码就需要通知编译器可能会抛出IOException类的异常。

方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类已检查的异常。例如
 public FileInputStream(String name) throws FileNotFoundException

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

在自己编写方法时,不必将所有可能抛出的异常都进行声明。至于什么时候需要在方法中用throws子句声明异常,什么异常必须使用throws子句声明,需要记住在遇到下面4种情况时应该抛出异常。
1)调用一个抛出已检查异常的方法,例如,FileInputStream构造器
2)程序过程中发现错误,并且利用throw语句抛出一个已检查异常
3)程序出现错误,例如a[-1]=0会抛出一个ArrayIndexOutOfBoundsException这样的未检查异常。
4)Java虚拟机和运行时库出现的内部错误。
如果出现前两种情况之一,则必须告诉调用这个方法的程序员有可能抛出的异常。因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果没有处理器捕获这个异常,当前线程就会结束

对于那些可能被他人使用的Java方法,应该根据异常规范,在方法的首部声明这个方法可能抛出的异常。
classs MyAnimation
     . . .
    public Image loadImage(String s) throws IOException
    {
        . . . 
    }
如果一个方法有可能抛出多个已检查异常,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。
classs MyAnimation
     . . .
    void drawImage(int i ) throws FileNotFoundException,EOFException
    {
        . . . 
    }

★我们不应该声明从RunntimeException继承的那些未检查的异常。
classs MyAnimation
     . . .
    public Image drawImage(int i )throws ArrayIndexOutOfBoundsException //bad style
    {
        . . . 
    }
这些运行时错误完全在我们的控制之下。如果特别关注数组下标引发的错误,就应该花更多的时间花费在修正程序的错误上,而不是说明这些错误发生的可能性上。
总之,一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控制(Error),要么就应该 避免发生(RunntimeException)。如果方法没有声明所有可能发生的已检查异常,编译器就会给出一个错误消息。

除了异常声明之外,还可以捕获异常。稍后将会讨论如何决定一个异常是被捕获,还是被抛出让其他的处理器进行处理

警告:如果子类中覆盖了超类的一个方法 子类方法中声明的已检查异常不能比超类方法中声明的异常更通用(只能一致或者是该父类方法异常的子类)特别是如果超类方法没有抛出任何已检查异常,子类也不能抛出任何已检查异常

11.1.3 如何抛出异常
假设在程序代码中发生了一件糟糕的事。一个名为readData的方法正在读取一个首部具有下列信息的文件:
Content-length:1024
然而读到733个字符后文件就结束了。我们认为这是一个不正常的情况,希望抛出一个异常。
首先要决定应该抛出什么类型的异常。将上述异常归结为IOException是一种很好的选择。仔细的阅读Java API文档之后会发现:EOFException异常描述的是“在输入过程中,遇到了一个未预期的EOF的信号”。这正是我们抛出的异常。下面是抛出这个异常的语句:
throw new EOFException();
或者
EOFException e = new EOFException();
throw e
下面将这些代码放在一起:
String readData(Scanner in)  throws EOFException
{
    . . .
    while(. . .)
    {
        if(!in.hasNext())    //EOF encountered
        {
            if(n < length)
                 throw new EOFException();
        }
          . . .
    }
    return s;
}
EOFException类还有一个字符串型参数的构造器,这个构造器可以更加细致的描述异常出现的情况。
String gripe = "Content-length"+len+",Received: "+n;
throw new EOFException(gripe);
在前面已经看到,对于一个已经存在的异常类,将其抛出非常容易。在这种情况下:
1)找到一个合适的异常类
2)创建这个类的一个对象
3)将对象抛出
一旦方法抛出了异常,这个方法就不可能返回到调用者。也就是说,不必为返回的默认值或错误代码担忧。

11.1.4 创建异常类
在程序中,可能会遇到任何标准异常类都没有能够充分的描述清楚的问题。在这种情况下,创建自己的异常类就是一件顺利成章的事情了。我们需要做的仅是一个派生于Exception的类。或派生于Exception子类的类。例如,定义个派生于IOException的类。习惯上定义的类应该包含两个构造器,一个是默认的构造器;另一个是带有详述信息的构造器。
class FileFormatException extends IOException
        public FileFormatException(){};
        public FileFormatException(String gripe)
        {
            super(gripe);
        }
现在就可以抛出自己定义的一场类型了。
String readData(BufferedReader in) throws FileFormatException 
{
    ...
    while(...)
    {
        if(ch==-1)
        {
            if(n<len)
                throw new FileFormatException();
        }
        . . .
    }
    return s;
}

11.2 捕获异常
到目前为止,已经知道如何抛出一个异常。这个过程十分容易。只要将其抛出就不用理睬了。当然,有些代码必须捕获异常。捕获异常需要进行周密的计划了。

如果某个异常发生的时候没有在任何地方进行捕获那程序就会终止执行,并在控制台上打印出异常信息。其中包括异常的信息和堆栈的内容。
要想捕获异常,必须设置try/catch语句块。最简单的try语句块如下所示:
try
{
   code  
   more code
   more code
}
catch(ExceptionType e)
{
    handler for this type
}
如果在try语句块中的任何代码抛出了一个catch子句说明的异常类,那么
1)程序将跳过try语句块的其余代码
2)程序将执行catch子句中的处理代码
如果在try语句块中的代码没有抛出任何异常,那么程序将跳过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();
    }
}
需要注意的是,try语句中的大多数代码都很容易理解:读取并处理问本行,直到遇到文件结束符为止。正如在Java API中看到的那样,read方法有可能抛出一个IOException异常。在这种情况下,将跳出整个while循环,进入catch子句,并生成一个栈轨迹。对于一个普通程序来说,这样的处理异常节本上合乎情理,还有其他的选择吗?
通常,最好的选择是什么也不做,而是将异常传递给调用者。如果read方法出现了错误,就让那个read方法的调用者去操心!如果采用这种方式,就必须声明这个方法可能会抛出一个IOException。
public void read(String filename) throws IOException
{
        InputStream in = new FileInputStream(filename);
        int b;
        while((b=in.read())!=-1)
        {
            process input
        }
  }
请记住,编译器严格地执行throws说明符。如果调用了一个抛出已检查异常的方法,就必须对它进行处理,或者将它继续进行传递。

哪种处理更好呢?通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递。如果想传递一个异常,就必须在方法的首部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。
仔细阅读一下Java API文档,以便知道每个方法可能会抛出的异常,然后再决定是自己处理,还是添加到throws列表中。对于后一种情况,也不必犹豫。将异常直接交给能够胜任的处理器进行处理要比压制对它的处理更好。

同时请记住这个规则也有一个例外。前面曾经提到过:如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个已检查异常。不允许在子类的throws说明符中出现超过超类方法所列出的异常类范围

11.2.1 捕获多个异常
在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可以按照下列方式为每个异常类型使用一个单独的catch子句。
try
{
    code that might throw exception
}
catch(FileNotFoundExcepiton e)
{
    emergency action for missing files
}
catch(UnKnowHostException e)
{
    emergency action for unknown host
}
catch(IOException e)
{
    emergency action for all  other I/O problems
}
异常对象可能包含与异常本身有关的信息。要想获得对象的更多信息,可以试着使用
e.getMessage()
得到详细的错误信息,或者使用
e.getClass().getName()
得到异常对象的实际类型

在Java SE 7 中,同一个catch子句中可以捕获多个异常类型。例如,假设对应缺少文件和未知主机异常的动作是一样的,就可以合并catch子句:
try
{    
    code that might throw exceptions
}
catch(FileNotFoundException | UnknownHostException e)
{
    emergency action for missing files and unknown host
}
catch(IOException e)
{
     emergency action for all  other I/O problems
 }
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。
注:捕获多个异常不仅会让你的代码看起来更简单,还会更高效。生成的字节码只包含一个对应公共catch子句的代块。

11.2.2 再次抛出异常与异常链
在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。如果开发了一个供其他程序员使用的子系统,那么,用于表示子系统故障的异常类可能会产生多种解释。ServletException就是这样一个一场类型的例子。执行servlet的代码可能不想知道发生错误的细节原因,但希望明确地知道servlet是否有问题。
下面给出了捕获异常并将它再次抛出的基本方法:
try
{
    access the database
}
catch(SQLException e)
{
    throw new ServletException("database error:" + e.getMessage());
}
这里ServletException用带有异常信息的文本的构造器来构造。不过,可以有一种更好的处理方法,并且将原始异常设置为新异常的“原因”:
try
{
    access the database
}
catch(SQLException e)
{    
    Thorwable 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;
}

11.2.3 finally子句(从异常开始说起)
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行

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

java有一种更好的解决方案,这就是fianlly子句。不管是否有异常被捕获,finally子句中的代码都被执行。

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

提示:这里强烈建议独立使用try/catch和try/finally语句块。这样可以提高代码的清晰度。例如:
InputStream in = ...;
try
{
    try
    {
        code that might throw exception
    }
    finally
    {
        in.close()
    }
}
catch(IOException e)
{
    show error message
}
内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告出现的错误。这种设计方式不仅清楚,而且还具有一个功能,就是将会报告finally子句中出现的错误。

警告:当finally子句包含return语句时,将会出现一种意想不到的结果。假设利用return语句从try语句块中退出。在方法返回前,fianlly语句块的内容将被执行。如果finally子句中也有一个return语句,这个返回值将会覆盖原始的返回值。请看一个例子:
public static int f(int n)
{
    try
    {
        int r = n * n ;
        return r;
    }
    finally
    {
        if(n==2) return 0
    }
}
如果调用f(2),那么try语句块的计算结果为r = 4,并执行return语句。然而,在方法真正返回前,还要执行finally子句。fianlly子句将使得方法返回0.这个返回值覆盖了原始的返回值4.

11.2.4 带资源的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")))
{
    while(in.hasNext())
        System.out.println(in.next());
}
这个块正常退出时,或者存在一个宜昌市,都会调用in.close()方法,就好像使用了finally块一样。
还可以指定多个资源。例如
try(Scanner in= new Scanner(new FileInputStream("/usr/share/dict/words")),
    Printer out = new PrintWriter("out.txt"))
{
    while(in.hasNext())
        System.out.println(in.next());
}
不论这个块如何退出,in和out都会关闭。如果你用常规方式手动编程,就需要两个嵌套的try/fianlly语句。

只要需要关闭资源,就要尽可能地使用带资源的try语句。

11.2.5 分析堆栈跟踪元素
堆栈跟踪(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。前面已经看到过这种列表,当Java程序正常终止,而没有捕获异常时,这个列表就会显示出来。
可以调用Throwable类的printStackTrace方法访问堆栈跟踪的文本描述信息。
Throwable t = new Throwable();
ByteArrayOutputStream out = new ByteArrayOutputStream();
t.printStackTrace(out);
String description = out.toString();
一种更灵活的方法是使用getStackTrace方法,它会得到StackTraceElement对象的一个数组,可以在你的程序中分析这个对象数组。例如:
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement[] frame:frames)
    anlanyze frame
StackTraceElement类含有能够获得文件名和当前执行的代码行号的方法,同时,还含有能够获得类名和方法名的方法。toString方法将产生一个格式化的字符串,其中包含所获得的信息。
静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈跟踪。下面给出这个方法的具体方式:
Map<Thread,StackTraceElement[]> map = Thread.getAllStackTraces();
for(Thread t : map.keySet())
{    
    StackTraceElement[] frames = map.get(t);
     anlanyze frame
}
打印堆栈信息


11.3 使用异常机制的技巧
1.异常处理不能代替简单的测试
2.不要过分的细化异常
3.利用异常层次结构
4.不要压制异常
5.在检测错误时,苛刻要比放任好
6.不要羞于传递异常。

11.5 记录日志
每个Java程序员都很熟悉在有问题的代码中插入一些System.out.println方法调用来帮助观察程序运行的操作过程。当然,一旦发现问题的根源,就要将这些语句从代码中删去。如果接下来又出现问题,就需要再插入几个调用System.out.println方法的语句。记录日志API就是为了解决这个问题而设计的。

11.5.1 基本日志
从一个最简单的例子开始:日志系统管理着一个名为Logger.global的默认日志记录器,可以用System.out替换它,并通过调用info方法记录日志信息。
Logger.getGlobal().info("File->Open menu item selected");
在默认情况下,这条语句将会显示出如下所示的内容:
May 10 2013 10:12:15 PM LoggingImageViewer fileOpen
INFO: File->Open menu item selected
注意:自动包含了时间、调用的类名和方法名。但是,如果在相应的地方(如main开始)调用
Logger.getGlobal().setLevel(Level.OFF);
将会取消所有日志。


11.5.2 高级日志
在一个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义日志记录器。
调用getLogger方法可以创建或检索记录器:
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
与包名类似,日志记录器也具有层次结构。事实上,与包名相比,日志记录器的层次性更强。对于包来说,一个包的名字与其福报的名字之间没有语义关系,但是日志记录器的父与子之间将共享某些属性。例如,如果对com.mycompany日志记录器设置了日志级别,它的子记录器也会继承这些级别。
通常,有以下7个日志记录级别:
--severe
--warning
--INFO
--FINE
--FINER
--FINEST
在默认的情况下,只记录前三个级别。也可以设置其他的级别。例如
logger.setLevel(Level.FINE);
另外,还可以使用Level.ALL开启所有级别的记录,或者使用Level.OFF关闭所有级别的记录。
对于所有的级别有下面几种记录方法:
logger.warning(message);
logger.fine(message);
同时,还可以使用log方法指定级别,例如:logger.log(Level.FINE,message);


11.6 调试技巧
1)可以用下列方法打印或记录任意变量的值,记得为每个自定义类提供toString方法
System.out.println("x="+x);
Logger.getGlobal().info("x="+x);

2)一个技巧是在每一个类中放置一个main方法。这样就可以对每一个类进行单元测试
public class MyClass
{
    methods and fields
    . . .
    public static void main(String[] args)
    {
        test code
    }
}
利用这种技巧,只需要创建少量对象,调用所有方法,并检测每个方法是否能够正确运行就可以了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值