JAVA——异常

文章目录

        • 程序发生异常,至少应该做到以下几点:
        • 异常的继承关系
        • throws声明受查异常
            • 抛出异常的几种情况
            • 超类和子类的异常声明
        • 抛出异常
        • 捕获异常
            • 多catch语句的合并
        • 再次抛出异常
        • finally子句
        • 带资源的try语句
        • 异常机制的使用技巧
            • 异常处理不能替代简单的测试
            • 不要过分细化异常
            • 利用异常层次结构
            • 不要压制异常
            • 在检测错误时,严苛比放任更可取
            • 不要羞于传递异常

程序发生异常,至少应该做到以下几点:

  • 向用户报告错误
  • 保存所有的工作结果
  • 允许用户以妥善的形式退出程序

异常的继承关系

java.lang.Throwable
	|--Error	//内部错误引起,只能通告给用户,并尽力使程序安全地终止
	|--Exception	//可被处理或抛出的异常
		|--RuntimeException		//程序错误导致的异常,只能处理,不能throws
		|--otherException		//程序以外的问题导致的异常,可处理可throws

可以看到,所有ErrorException拥有共同的父类——Throwable,用户自定义的异常类也必须继承自Throwable,只有Throwable及其子类才可以用throw/throws关键字抛出。
Java语言规范将派生于ErrorRuntimeException类的异常称为非受查(unchecked)异常,其他所有异常被称为受查(checked)异常。区别在于,对于非受查异常,Java编译器不要求捕获或一定要抛出,但必须要捕获或抛出受查异常。

public class Test {
    public static void main(String[] args) {
        Test p = new Test();
        p.done();
    }
 
    public void done() throws ArithmeticException{
        System.out.println();
    }
}

如,done()方法声明抛出的是ArithmeticException extends RuntimeException,属于非受查异常,所以main()函数可以不去处理。

throws声明受查异常

public FileInputStream(String name)throws FileNotFoundException{
	...
}

如上,throws声明表示FileInputStream这个构造器可能会抛出一个FileNotFoundException类对象。

抛出异常的几种情况

1)调用一个抛出异常的方法,如FileInputStream构造器
2)程序中发生错误,并且利用throw语句抛出一个受查异常
3)程序出错,如,a[-1] = 0会抛出ArrayIndexOutOfBoundsException非受查异常
4)Java虚拟机和运行时库出现的内部错误

其中,1)2)必须在首部声明这些方法可能抛出异常,3)指的是从RuntimeException继承的异常,应该尽量避免此类异常发生,而不是说明这些异常发生的可能性,4)其实就是Error,我们对其没有任何控制能力。

所以,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控Error,要么应该避免RuntimeException,不需要声明异常发生的可能性。

public Image loadImage(String s)throws FileNotFoundException, EOFException	//抛多个异常
{
	...
}
超类和子类的异常声明

如果子类覆盖了超类中的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用,即,如果超类方法抛出IOExceptipn,那么子类方法只能抛出IOException的子类异常,或者根本不抛任何异常。

抛出异常

String readData(Scanner in)throws EOFException
{
	...
	while(...)
	{
		if(!in.hasNext())	//EOF encountered
		{
			if(n < len)
				throw new EOFException;
		}
		...
	}
	return s;
}

1)选择合适的异常类或创建一个异常类
2)创建该类的一个对象
3)抛出

throw语句执行后,其后的语句不再执行,finally中的语句除外。

捕获异常

try/catch/finally语句块来捕获异常

/*完整形式*/
try
{
	code
	more code
}
catch(ExceptionType e)
{
	handler for this type
}
finally
{
	code that must be executed
}
/*一般形式*/
try
{
	...
}
catch(ExceptionTyepe e)		//可以有多个catch捕获不同的异常
{
	...
}
/*其他形式*/
try
{
	...
}
finally
{
	...
}

try语句块中方法的任何代码抛出的异常类型必须在catch子句中声明了,程序才会跳转到对应catch子句,如果抛出的异常类型没有声明的话,那么这个方法就会立刻退出。
捕获或抛出异常需要谨慎选择,如果是知道如何处理的异常,就应该捕获,而那些不知道如何处理的异常则应该继续向外抛出去。
有一种例外的情况,即编写一个覆盖超类的方法,而被覆盖的超类方法没有抛出异常,那么子类方法就必须捕获方法代码中可能出现的每一个受查异常,而不能throws抛出。

多catch语句的合并

Java SE 7以后,同一个catch子句中可以捕获多个异常类型。

try
{
	code that might throw exception
}
catch(FileNotFoundException | UnknownHostException e)
{
	emergency action for missing files and unknown hosts
}

如上,FileNotFoundExcepitonUnknownHostException两个异常捕获子句合并为一个,即发生两个之中任意一个异常时,都会执行子句体中的内容。
但是,捕获多个异常时,异常变量e隐含为final变量,不能再为e赋不同的值。

再次抛出异常

catch子句中throw一个异常,这样做可以改变异常的类型,实现异常转换的效果。一般用于封装某一模块时,不想直接提供具体错误的细节原因,而是用一个更抽象的异常来描述错误信息,如ServletException

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

在上述例子中,向外抛出具体的异常,如SQLException,过于细节了,这是调用者不关注的,调用者关注的只是从Servlet层面上发生了什么问题,所以我们可以通过再次抛出异常将具体异常转换成带有异常信息文本的超类异常,即ServletException

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

更优的处理方法如上,不是直接抛出原异常的超类异常,而是将原始异常设置为新异常(超类异常)的cause,这样在捕获到异常的时候,仍然可以通过处理重新得到原始异常。

Throwable e = se.getCause();

这是一种包装技术,既能使抛出的异常不过于局限在某一细节上,又不会丢失原始异常的细节,随时可以回溯。

finally子句

finally子句的特点是,无论是否有异常被捕获,子句中的代码都会被执行,常被用来执行资源回收相关的语句。
一般的用法是,try/catch/finally连用,try/catch捕获异常,finally关闭资源,但是这种情况会造成强耦合,所以一般建议分开处理,提高代码清晰度:

InputStream in = ...;
try
{
	try
	{
		code that might throw exception
	}
	finally
	{
		in.close();
	}
}
catch(IOException e)
{
	show error message
}

这样解耦合处理,使内层的try/finally语句块只用来确保关闭输入流,而外层的try/catch语句块只用来确保捕获报告中出现的错误。这种设计思路清晰,同时,还能报告finally子句中可能出现的错误。

public static int f(int n)
{
	try
	{
		int r = n * n;
		return r;	//执行此句之前,会先执行finally子句中的内容
	}
	finally
	{
		if(n == 2)
			return 0;	//如果执行到此句,将会直接返回值,结束f()函数,不再跳回try子句中执行return语句
	}
}

所以,会出现f(3) = 9f(2) = 0,两种不同的返回情况。

带资源的try语句

针对实现了AutoCloseableCloseable接口的资源类,接口中规定了对应的close()方法:

public interface AutoCloaseable
{
	public void close() throws Exception;
	...
}
public interface Closeable
{
	public void close throws IOException;
	...
}

在使用实现了以上两种接口的资源类时,可以使用带资源的try语句

try(Resource res1 = ...; Resource res2 = ...; ...)
{
	work with res1
	work with res2
	...

try块正常退出时,或存在一个异常时,都会自动调用res.close()方法,等同于使用了finally块。
普通的try/finally语句中,如果tryfinally块抛出了同样的异常,那么finally中的异常会覆盖掉try中的异常,而带资源的try语句可以很好的避免这个问题。
在带资源的try语句中,发生上述情况时,原来的异常(try块中的异常)会重新抛出,而close()方法(finally块中)抛出的异常会“被抑制”。这些异常将被自动捕获,并由addSuppressed方法添加到原来的异常,如果对这些异常感兴趣,可以调用getSuppressed方法得到从close方法抛出并被抑制的异常列表。

异常机制的使用技巧

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

如尝试上百万次地对一个空栈执行退栈操作,在实施退栈操作之前,首先查看栈是否为空。有两种写法:

//第一种
if(!s.empty())
{
	s.pop();
}
//第二种
try
{
	s.pop();
}
catch(EmptyStackException e)
{
}

结果是,调用isEmpty的版本的运行时间为646毫秒,捕获EmptyStackException的版本运行时间为21739毫秒,采用异常机制花费的时间大大超过了常规写法,所以使用异常的基本规则是:只有在异常情况下使用异常机制。

不要过分细化异常

PrintStream out;
Stack s;

for(int cnt = 0; cnt < 100; cnt ++)
{
	try
	{
		n = s.pop();
	}
	catch(EmptyStackException e)
	{
		//stack was enpty
	}
	try
	{
		out.writeInt(n);
	}
	catch(IOException e)
	{
		//problem writing to file
	}
}

这种编程方式将导致代码量的急剧膨胀,而且执行速度比较慢。所以,有必要把整个任务包装在一个try语句块中,这样当任何一个操作出现问题时,整个任务都可以取消。

try
{
	for(int cnt = 0; cnt < 100; cnt++)
	{
		n = s.pop();
		out.writeInt(n);
	}
}
catch(IOException e)
{
	//problem writing to file
}
catch(EmptyStackException e)
{
	//stack was empty
}
利用异常层次结构

不要只抛出RuntimeException异常,应该寻找更加适当的子类或创建自己的异常类。
不要只捕获Throwable异常,否则,会使程序代码晦涩难懂。
适当地将一种异常转换成另一种更加适合的异常。如,在解析某个文件中的一个整数时,捕获NumberFormatException异常,然后将它转换成IOExceptionMySubsystemException

不要压制异常
在检测错误时,严苛比放任更可取

举个例子,当栈空时,Stack.pop是返回一个null,还是抛出一个异常?
通常,我们认为,在出错的地方抛出一个EmptyStackException异常要比在后面抛一个NullPointException异常更可取。

不要羞于传递异常

并不是要尽可能多得捕获异常。如果调用了一个抛出异常的方法,例如,FileInputStream构造器或readLine方法,这些方法就会本能地捕获这些可能产生的异常。其实,传递异常要比捕获这些异常更好:

public void readStuff(String filename) throws IOException  //not a sign of shame
{
	InputStream in = new FileInputStream(filename);
	...
}

让更高层次的方法通知用户发生了错误,或者放弃不成功的命令更加合适。

你可能感兴趣的:(JAVA)