CoreJava读书笔记--异常、断言和日志(一)--异常

异常

在理想状态下,我们认为用户的输入永远是正确的,选择打开的文件也是一定存在的,并且永远不会出现BUG,但这仅仅是理想状态。但现实状况却充满了不良的数据和带有问题的代码。为了避免这类事情的发生,至少应该做到以下几点:

①向用户通告错误

②保存所有的工作结果

③允许用户以妥善的形式退出程序

(一)异常分类

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

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

Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这类对象。如果出现了这样的内部错误,除了告知用户,并尽力使程序安全退出外,再也无能为力了。这种情况很少出现。

Exception类层次结构又分为两支,一支是RuntimeException,另一个是其他异常。划分两支的规则是:由于程序错误导致的异常是RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。

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

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

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

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

“如果出现RuntimeException异常,那么就一定是你的问题。”是一条相当有道理的规则。

Java语音规范将派生于Error类或RuntimeException类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked)异常。编译器将核查是否为所有的受查异常提供了异常处理器。

(二)声明受查异常

如果遇到了无法处理的情况,那么Java方法可以抛出一个异常。这个道理很简单,一个方法不仅要告诉编译器需要返回一个什么值,还要告诉编译器有可能发生什么错误。

方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类受查异常。看个例子:

public FileInputStream(String name) throws FileNotFoundException

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

在自己写方法时,什么时候需要在方法中用throws子句声明异常,什么异常必须使用throws子句声明,需要记住以下几种情况需要抛出异常:

①调用一个抛出受查异常的方法

②程序运行过程中发现错误,并且利用throw语句抛出一个受查异常

③程序出现错误

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

如果出现前两种情况之一,就必须告诉调用这个方法的程序员有可能抛出异常。因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果处理器没有捕获这个异常,当前执行的线程就会结束。

对于那些可能被他人使用的Java方法,应该根据异常规范(exception specification),在方法的首部声明这个方法可能抛出的异常。

class  MyAnimation{
    ...
    public Image loadImage(String s) throws IOException{
       ...
    }
}

如果一个方法有可能抛出多个异常,那么就要在方法的首部列出所有的异常类,每个异常类用逗号隔开。

class MyAnimation{
    ...
    public Image loadImage(String s) throws FileNotFoundException,IOException{
        ...
    }
}

但是不需要声明Error,因为我们对其没有控制能力,也不应该声明从RuntimeException继承的那些非受查异常。

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

警告:在继承中,如果子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用。(也就是说子类要声明更特定的异常或者不抛出任何异常)。如果超类方法没有抛出异常,那么子类方法也不能抛出任何异常。

(三)如何抛出异常

对于一个已经存在的异常类,要抛出一个异常很简单:

①找到一个合适的异常类

②创建这个类的对象

③将对象抛出

throw new EOFException;

或者是

EOFException e = new EOFException();

throw e;

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

(四)创建异常类

当标准库中没有异常类能够充分的描述清楚问题,那么我们可以自定义异常类。自定义异常类需要做的只是定义一个派生于Exception的类,或者派生于Exception类的子类。

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

习惯上,定义的类应该包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器。(超类Throwable的toString方法将会打印出这些详细信息,这个在调试中非常有用)。

现在可以抛出自己定义的异常类型了:

String readData(BufferedReader in) throws FileFormatException
{
    ...
    while(...){
        if(ch==-1)//EOF{
            if(n<len){
                throw new FileFormatException();
            }
        }
    }
    return s;
}

捕获异常

(一)捕获异常

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

要想捕获一个异常,必须设置try/catch语句块。

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

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

①程序将跳过try语句块中的其余代码

②程序将执行catch子句中的处理器代码

如果在try语句中没有抛出任何异常,那么程序将跳过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方法出现了错误,就让read方法的调用者去操心。如果采用这种处理方式,就必须声明这个方法会抛出一个IOException。

public void read(String filename) throws IOException
{
    InputStream in = new FileInputStream();
    int b;
    while((b=in.read))!=-1)
    {
        process input
    }
}

请记住,编译器严格地执行throws说明符。如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递。

前面提到过如果超类方法没有抛出任何异常,那么这个方法也不能抛出任何异常,只能捕获方法代码中出现的每一个受查异常。

(二)捕获多个异常

在一个try语句块中,可以捕获多个异常类型,并对不同类型的异常做出不同的处理。

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

在Java7中,同一个catch子句中可以捕获多个异常类型。假如FileNotFoundException和UnknownHostException的动作是一样的,就可以写成:

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
}

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

(三)再次抛出异常与异常链

在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。下面给出了捕获异常并将它再次抛出的基本方法:

try
{
    access the database
}
cathc(SQLException e)
{
    throw new ServletException("database error : "+e.getMessage());
}

不过这里也有一种更好的处理方法,并且将原始异常设置为新异常的“原因”:

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 exceptions
    //2
}
catch(IOException e)
{
    //3
    show error message
    //4
}
finally
{
    //5
    in.close();
}
//6

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

(1)代码没有抛出异常。这种情况下,会执行try语句块里全部代码,然后执行finally子句中的代码,全部执行完后就会执行try语句块后第一条语句。也就是说:执行标注的1、2、5、6。

(2)抛出一个在catch子句中捕获的异常。先执行try语句块里的代码,当异常发生的时候,会跳过try语句块里的剩余代码,然后执行catch子句中的代码,最后执行finally子句中的代码块。也就是1、3、4、5、6。

        如果catch子句中没有抛出新的异常,异常将被抛回这个方法的调用者。在这里,执行1、3、5处的语句。

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

try语句块也可以只有finally子句,而没有catch子句。例如:

InputStream in = new FileInputStream(...);
try
{
    code that might throw exceptions
}
finally
{
    in.close();
}

事实上,我们认为在需要关闭资源时,用这种方式使用finally子句是一种不错的选择。这里,强烈建议解耦合try/catch和try/finally语句块。这样可以提高代码的清晰度。例如:

//这种方式不仅设计清楚,还有一个功能就是将会报告finally子句中出现的错误

try   //外层try语句块也只有一个职责,就是确保报告出现的错误
{
    try   //内层try语句块只有一个职责,就是确保关闭输入流
    {
        code that might throws exceptions
    }
    finally
    {
        in.close();
    }
}
catch(IOException e)
{
    show error message
}

(五)带资源的try语句

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

也可以指定多个资源,例如:

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().toUpperCase());
    }
}

不论这个块如何退出,in和out都会关闭。所以只要需要关闭资源,就要尽可能使用带资源的try语句。

(六)分析堆栈轨迹元素

堆栈轨迹(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 Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame : frames)
{
    analyze frame
}

③静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈轨迹。下面就是多线程时,使用这个方法的具体方式:

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

下面的程序打印了递归阶乘函数的堆栈情况。

package stackTrace;

import java.util.Scanner;

public class StackTraceTest {
	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		System.out.println("Enter n : ");
		int n = in.nextInt();
		factorial(n);
	}
	
	public static int factorial(int n) {
		System.out.println("factorial("+n+"):");
		Throwable t = new Throwable();
		StackTraceElement[] frames = t.getStackTrace();
		for(StackTraceElement frame : frames) {
			System.out.println(frame);
		}
		int r;
		if(n<=1) {
			r=1;
		}else {
			r=n*factorial(n-1);
		}
		System.out.println("return "+r);
		return r;
	}
}

猜你喜欢

转载自blog.csdn.net/zxyy2627/article/details/82783188