Java基础—异常

1、异常分类

在这里插入图片描述
所有的异常对象都是由Throwable继承而来。

在下一层中分解为两个分支:ErrorException
Error类层次结构描述了程序无法处理的错误,如Java运行时系统的内部错误和资源耗尽错误。大多数错误与代码编写者的操作无关,而是代码运行时JVM出现的问题。这些异常发生时,JVM一般会选择线程终止。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。(Error由JVM产生并抛出,程序不需进行处理。

Exception层次结构又分解为两个分支:一个派生于RuntimeException,另一个包含其他异常。划分这两个分支的规则是:由程序逻辑错误导致的异常属于RuntimeException,而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。(如果出现RuntimeException异常,那么就一定是你的问题!)

  • 派生于RuntimeException的异常示例:错误的类型转换、数组访问越界、访问null指针
  • 不是派生于RuntimeException的异常示例:试图在文件尾部后面读取数据、试图打开不存在的文件、试图根据给定的字符串查找Class对象,而该字符串表示的类并不存在。

Java语言规范将派生于Error类或RuntimeException类的所有异常称为不可查(unchecked)异常,所有其他的异常称为可查(checked)异常

  • 对于可查异常,编译器要求必须处置,也就是说当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。编译器将核查是否为所有的受查异常提供了异常处理器。
  • 对于不可查异常,编译器不要求强制进行处置,它们要么不可控制(Error),要么应该避免发生(RuntimeException)
自定义异常:

当我们在代码中需要抛出异常时,尽量使用JDK已定义的异常类型。例如,参数检查不合法,应该抛出IllegalArgumentException

在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。

一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常

BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生:

public class BaseException extends RuntimeException {
}

其他业务类型的异常就可以从BaseException派生:

public class UserNotFoundException extends BaseException {
}

public class LoginFailedException extends BaseException {
}

...

自定义的BaseException应该提供多个构造方法:

public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(String message) {
        super(message);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }
}

上述构造方法实际上都是原样照抄RuntimeException。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。

2、异常处理

2.1 异常声明—throws

如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用throws子句来声明抛出异常。
throws语句用在方法定义时声明该方法要抛出的异常类型:

methodname throws Exception1,Exception2,..,ExceptionN  {  }

方法名后的throws Exception1,Exception2,…,ExceptionN 为声明要抛出的异常列表。当方法抛出异常列表的异常时,方法将不对这些类型及其子类类型的异常作处理,而抛向调用该方法的方法,由它去处理。
使用throws关键字将异常抛给调用者后,如果调用者不想处理该异常,可以继续向上抛出,但最终要有能够处理该异常的调用者。

异常抛出的规则:

  1. 如果是不可查异常,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
  2. 如果一个方法可能出现可查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误。
  3. 只有当抛出了异常时,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出。
  4. 方法覆盖时异常抛出规则
    1)如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用。(也就是,子类方法中可以抛出更特定的异常,或者根本不抛出任何异常。)
    2)如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。
    3)如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常,或者这个类的任意一个子类的异常。
2.2 异常抛出—throw

throw总是出现在方法体中,用来抛出一个Throwable类型的异常。程序会在throw语句后立即终止,它后面的语句执行不到,然后在包含它的所有try块中(可能在上层调用函数中)从里向外寻找含有与其匹配的catch子句的try块。
如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。

对于一个异常对象,真正有用的信息是异常的对象类型,而异常对象本身毫无意义。比如一个异常对象的类型是ClassCastException,那么这个类名就是唯一有用的信息。所以,在选择抛出什么异常时,最关键的就是选择异常的类名能够明确说明异常情况的类。
异常对象通常有两种构造函数:一种是无参数的构造函数;另一种是带一个字符串的构造函数,这个字符串将作为这个异常对象除了类型名以外的额外说明。

2.3 异常捕获—try/catch
2.3.1 异常捕获

在Java中,异常通过try-catch语句捕获。其一般语法形式为:

try {  
   // 可能会发生异常的程序代码  
} catch (Type1 id1){  
   // 捕获并处置try抛出的异常类型Type1  
} catch (Type2 id2){  
    //捕获并处置try抛出的异常类型Type2  
}

关键词try后的一对大括号将一块可能发生异常的代码包起来,称为监控区域

Java方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之外,由Java运行时系统试图寻找匹配的catch子句以捕获异常。若有匹配的catch子句,则运行其异常处理代码,try-catch语句结束。

匹配的原则是:如果抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与catch块捕获的异常类型相匹配。

注意:一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束。其他的catch子句不再有匹配和捕获异常类型的机会

Java通过异常类描述异常类型,对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。
例如:RuntimeException异常类包括运行时各种常见的异常,ArithmeticException类和ArrayIndexOutOfBoundsException类都是它的子类。因此,RuntimeException异常类的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();
	}
}

除在程序中捕获异常并进行处理外,通常更好的选择是什么都不做,而是将异常传递给调用者。如果采取这种处理方式,就必须声明这个方法可能会抛出一个IOException。

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

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

通常,应该捕获那些知道如何处理的异常,而将那些不知道怎么处理的异常继续进行传递。但该规则也有一个例外:前面提到过,如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每个受查异常。

2.3.2 捕获多个异常

在Java SE 7中,同一个catch子句中可以捕获多个异常类型。例如对应缺少文件和未知主机异常的动作是一样的,就可以合并catch子句:

try {
	//code that might throw exceptions
}
catch (FileNotFoundException | UnknownHostException e) {
	//emergency code 1
}
catch (IOException e) {
	//emergency code 2
}

只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。另外,当捕获多个异常时,异常变量隐含为final变量。例如不能在上面code 1中为e赋不同的值。

2.3.3 再次抛出异常与异常链

在catch子句中可以抛出一个异常,目的是改变异常的类型。如果开发了一个供其他程序员使用的子系统,那么用于表示子系统故障的异常类型可能会产生多种解释。ServletException就是这样一个异常类型的例子。执行Servlet的代码可能不想知道发生错误的细节原因,但希望明确知道servlet是否有问题。

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

这里ServletException用带有异常信息文本的构造器来构造。不过还有一种更好的处理方式,并且将原始异常设置为新异常的“原因”:

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

当捕获到异常时,可使用Throwable e = se.getCause();得到原始异常。强烈使用这种包装技术,可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

2.3.4 finally子句

利用finally子句对已分配资源回收或关闭数据库连接

InputStream in = new FileInputStream();
try {
	//1
	code
	//2
}
catch {
	//3
	show error message
	//4
}
finally {
	//5
	in.close();
}
//6

几种情况:

  1. 代码未发生异常:1、2、5、6
  2. 抛出一个在catch子句中捕获的异常:
    • catch子句中没有抛出异常:1、3、4、5、6
    • catch子句中抛出异常:1、3、5
  3. 代码抛出一个异常,但该异常不是由catch子句捕获的:1、5

注意:
1、try子句可以只有finally子句,而没有catch子句。
2、当try-catch-finally中包含return语句时,程序的执行和返回情况:
JavaSE之彻底搞懂try,catch,finally与return的执行
谈谈Java中try-catch-finally中的return语句
JAVA异常及其异常处理方式
3、如果在finally块中抛出异常,try块捕捉的异常就不能抛出,外部捕捉到的异常就是finally块中的异常信息,而try块中发生的真正的异常堆栈信息则丢失了。
4、在下述4种特殊情况时,finally块都不会被执行:
1)在finally语句块中发生了异常。
2)在前面的代码中用了System.exit()退出程序。
3)程序所在的线程死亡。
4)关闭CPU。

2.3.5 带资源的try语句

对通过以下代码模式关闭资源:

open a resource
try {
	work with the resource
}
finally {
	close the resource
}

存在两种问题:(1)略繁琐(2)假设在try语句块中抛出了一些非IOException的异常,这些异常只有该方法的调用者才能处理。同时,调用close方法有可能抛出IOException异常。当出现这种情况时,原始的异常将会丢失,转而抛出close方法的异常,而这经常并不是我们想要的。

假设资源属于一个实现了AutoCloseable接口的类,Java SE 7为这种代码模式提供了一个很有用的快捷方式。AutoCloseable接口有一个方法:
void close() throws Exception
带资源的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());
}

这个块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用了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().toUpperCase());
}

与此同时,带资源的try语句可以很好的处理close语句抛出的异常覆盖try语句抛出的异常这种情形。使用带资源的try语句,原来的异常会重新抛出,而close方法抛出的异常会“被抑制”。这些异常将自动捕获,并由addSuppressed方法增加到原来的异常。如果对这些异常感兴趣,可调用getSuppressed方法,它会得到从close方法抛出并被抑制的异常列表 。

2.3.6 分析堆栈轨迹元素

见《Java核心技术 卷I》P280

本博客参考自:《Java核心技术 I》

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值