黑马程序员_异常

-------android培训java培训、期待与您交流! ----------

在Java程序设计语言中,异常对象都是派生于Throwable类的一个实例:


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

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

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

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

1)     错误的类型转换

2)     数组访问越界

3)     访问空指针

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

1)     试图在文件尾部后面读取数据

2)     试图打开一个错误格式的URL

3)     视图根据给定的字符串查找Class对象,而这个字符串表示的类不存在

Java语言规范将派生于Error或RuntimeException类的所有异常称为未检查异常(unckecked),所有其他的异常称为已检查异常(checked),编译器将检查是否所有的已检查异常提供了异常处理器。

      PS:RuntimeException这个名字很容易让人混淆,实际上,现在讨论的所有错误都发生则在运行时刻。

 

声明已检查异常

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

      方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类已检查异常。例如,下面是标准库提供的FileInputStream类的一个构造器声明:

publicFileInputStream(String name) throws FileNotFoundException

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

 

在自己编写方法时,不必将所有可能抛出的异常都进行声明,需要记住下面的4种情况时应该抛出异常:

1)     调用一个抛出已检查异常的方法,例如FileInputStream构造器

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

3)     程序出现错误,例如a[-1]=0会抛出一个ArrayIndexOutOfBoundsException这样的未检查异常

4)     Java虚拟机和运行时库出现的内部异常

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

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

class MyAnimation
{
	//省略部分代码
	public Image loadImage(String s) throws IOException
	{
		//省略部分代码
	}
}

如果一个方法有可能抛出多个已检查异常,那么必须要在方法的首部列出所有的异常类,每个异常类之间使用逗号隔开。

public Image loadImage(String s) throws EOFException , MalformedURLException

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

PS:如果在子类中覆盖了超类的一个方法,子类方法声明中的已检查异常不能超过超类方法声明的异常范围,特别需要说明的是,如果超类方法没有抛出任何已检查异常,子类也不能抛出任何已检查异常。

 

如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出这个类的异常,或者这个类的任意一个子类的异常。例如,FileInputStream构造器声明将有可能抛出一个IOException异常,然而并不知道具体是哪种IOException异常。它既有可能是IOException异常,也可能是其子类的异常,例如,FileNotFoundException。

 

如何抛出异常

      假设一个名为readData的方法正在读取一个首部具有下列信息的文件:

       Content-length:1024

      然而,读到了733个字符之后文件就结束了。我们认为这是一种不正常的情况,希望抛出一个异常。

      首先要决定抛出异常的类型。阅读了JavaAPI之后发现:EOFException异常的描述是“在输入过程值,遇到了一个未预期的EOF后的信号。”这正是我们要抛出的异常。下面是抛出这是异常的语句:

throw new EOFException();

或者

EOFExceptione e = new EOFException();
throw e;

下面将这些代码放在一起:

String readData(Scanner in) throws EOFException{
	//省略部分代码
	while(//省略部分代码)
	{
		if(!in.hasNext()) //EOF encountered
		{
			if(n < len)
				throw newEOFException();
		}
		//省略部分代码
	}
}

      EOFException类还有一个含有一个字符串型参数的构造器。这个构造器可以更加细致的描述异常出现的情况:

String gripe = "Content-length:" + len + ",Received:" + n;
throw new EOFException(gripe);

      对于一个已经存在的异常类,将其抛出非常容易:

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

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

3)     将对象抛出

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

 

创建异常类

      在程序中,可能会遇到任何标准异常类都没有能够充分描述清楚的问题。在这种情况下,我们可以创建自己的异常类。我们需要定义一个派生于Exception的类,或者派生于Exception子类的类。例如,定义一个派生于IOException的类。习惯上,定义的类应该包含两个构造器:一个是默认的构造器,另一个是带有详细描述信息的构造器(超类Throwable的toString方法将会打印出这些详细信息,这在调试中非常有用)。

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

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

String readDate(ButteredReader in) throws EOFException
{
	//省略部分代码
	while(//省略部分代码)
	{
		if(ch == -1)  //EOF encountered
		{
			if(n < len)
				throw newFileFormatException();
		}
		//省略部分代码
	}
	return s;
}

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

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

try
{
	//省略部分代码
}
catch(Exception e)
{
	//handler for this type
}

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

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

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

如果在try语句块中没有抛出任何异常,那么程序将调过catch子句。

如果方法中的任何代码抛出了一个catch子句中没有声明的异常类型,那么这个方法就会立刻退出。

 

为了演示捕获异常的处理过程,下面给出一个读取文本的典型程序代码:

public void read(String filename)
{
	try
	{
		InputStream in = newFileInputStream(filename);
		int b;
		while ((b = in.read) != -1)
		{
			process input
		}
	}
	catch (IOException exception)
	{
		 exception.printStackTrace();
	}
}

      正如Java API中看到的那样,read方法可能抛出一个IOException异常。在这种情况下,将跳出整个while循环,进入catch子句,并输出堆栈情况。对于一个普通的程序来说,这样处理基本上合乎情理。

      但是,通常最好的选择是什么都不做,选择将异常传递给调用者,如果read方法出现了错误,就让read方法的调用者去操心,如果采用这种处理方式,就必须声明这种方法可能抛出一个IOException。

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


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

      哪种方法更好呢?通常,应该捕获那些知道如何处理的异常,而将那些不知道如何处理的异常传递出去。如果要想将异常传递出去,就必须在方法的顶部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。

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

 

捕获多个异常

      在一个try语句中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可以按照下列的方式为每个异常类型使用一个单独的catch子句。

try
{
	code that might throw exceptions
}
catch(MalformedURLException e1)
{
	emergency action for malformed URLs
}
catch(UnknownHostException e2)
{
	emergency action for unknown hosts
}
catch(IOException e3)
{
	emergency action for all other I/Oproblems
}

      异常对象(e1,e2,e3)可能包含与异常本身有关的信息。要想获得对象的更多信息,可以试着使用:

e3.getMessage()

得到详细的错误信息(如果有的话),或者使用

e3.getClass().getName()

得到异常的实际类型。

 

再次抛出异常与异常链

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

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

      这里,ServletException用带有异常信息的文本的构造器来构造。在Java SE 1.4中,可以有一种更好的处理方法,将原始异常设置为新异常的“诱饵”:

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

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

Throwable e = se.getCause();

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

      PS:如果在一个方法发生了一个已检查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个异常,并将它包装成一个运行时异常。

      PS:有些异常类,例如ClassNotFoundException、InvoTargetException和RuntimeException,拥有自己的异常链方案。在Java SE 1.4中,这些类已经引入“诱饵”机制。然而,仍然可以利用原来的方式,或者调用getCause得到异常链。

 

Finally子句

      当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且这有这个方法自己知道,而这些资源在退出方法之前必须被回收,那么就会产生资源回收问题。

      可以通过finally子句解决这个问题。下面将介绍如何恰当地释放Graphics对象。如果使用Java编写数据库程序,就需要使用这个技术关闭与数据库的连接。当发生异常时,恰当的关闭所有数据库连接是非常重要的。

      不管是否有异常被捕获,finally子句中的代码都被执行。在下面的示例中,程序将释放所有环境中的图形设备文本。

Graphicsg = image.getGraphics();
try
{
	//1
	code that might throw exception
	//2
}
catch(IOException e)
{
	//3
	show error dialog
	//4
}
finally
{
	//5
	g.dispose();
}
//6

在上面这段代码中,会出现一下几种情况:

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

2)     抛出一个在catch子句中捕获的异常,在这种情况下,程序将执行try语句块中的代码,知道发生异常为止。此时,将调过try语句块中的剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的语句。

如果catch子句没有抛出异常。执行1、2、4、5、6处的语句。

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

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

 

try语句可以只有finally子句,而没有catch子句。例如,下面这条try语句:

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

      无论在try语句块中是否遇到异常,finally子句中的in.close()语句都会执行。当然,如果真的遇到一个异常,这个异常将会被重新抛出,并且必须由另一个catch子句捕获。

      PS:这里,强烈建议独立使用try/catch和try/finally语句块。这样可以提高代码的清晰度:

InputStreamin = ...;
try
{
	try
	{
		code that might throw exceptions
	}
	finally
	{
		in.close();
	}
}
catch(IOException e)
{
	show error dialog
}

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

 

分析堆栈的跟踪元素

      堆栈跟踪(stack trace)是一个方法调用过程的列表,它包含了程序执行方法调用的特定位置。

      在Java SE 1.4以前的版本中,可以调用Throwable类的printStackTrace方法访问堆栈跟踪的文本描述信息。现在,可以调用getStackTrace方法获得一个StackTraceElement对象的数组,并在程序中对它进行分析。例如:

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

      StackTraceElement类含有能够获得文件名和当前执行代码行号的方法。同时,还含有能够获得类名和方法名的方法。toString方法将产生一个格式化的字符串,其中包含所获得的信息。

      在Java SE 5.0中,增加了一个静态的Thread.getAllStackTrace方法。它可以产生所有线程的堆栈跟踪。下面给出这个方法的具体方式:

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

使用异常机制的建议

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

与执行简单的测试相比,捕获异常所花费的时间大大超过了前者,因此,使用异常的规则是:只在异常情况下使用异常机制

2)     不要过分细化异常

3)     利用异常层次结构

将一种异常转换成另一种更加合适的异常时不要犹豫。例如,在解析某个文件中的一个整数时,捕获NumberFormatException异常,然后将它转换成IOException或MySubsystemException的子类。

4)     不要压制异常

在Java中,存在强烈的关闭异常的倾向。如果编写了一个调用一个方法的方法,而这个方法可能100年才抛出一个异常。那么,编译器会因为不将这个异常列在throws列表中而产生抱怨。而没有将这个异常列在throws表中主要出于编译器将会对所有调用这个方法的方法进行进行异常处理的考虑。因此,应该将这个异常关闭。

public Image loadImage(String s)
{
	try
	{
		code that might throw exceptions
	}
	catch (Exception e)
	{}     //so there
}

      现在。这段代码就可以通过编译了。除非发生异常,否则它将可以正常地运行。即使发生了异常也会被忽略。如果认为异常非常重要,就应该对它们进行处理。

5)     不要羞于传递异常

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值