面试|说说Java中的异常体系

1.什么是异常?为什么会有异常?

异常(Exception)是Java语言提出的一种错误报告模型,这种错误报告模型在程序和客户端之间传递异常问题。
我们应该意识到任何代码不可能是完全正确且永久正确,因此代码中必定存在异常或者错误;对于Java语言来讲,这种异常或者错误越早发现越好,即编译阶段就能把异常找出来;但因为异常的不确定性,所以有些异常有可能发生在运行时;另外,如果异常或者错误发生而没有得到处理,那么程序将会停止运行;所以为了提高代码的健壮性,有必要建立一套错误恢复机制,即错误报告模型。
使用异常处理(错误报告模型)的好处显而易见

  • 无论何时,代码都能可靠运行,即使发生异常,程序也能执行而不是停止
  • 异常处理使代码的阅读、编写和调试工作更加方便。试想一下,如果一个方法内部会发生异常而没有对异常进行处理,那么调用方法的每个地方都需要格外小心,而如果我在发生异常的方法内部把异常处理掉,那么调用方法的地方就不需要担心是否会发生异常了,这进一步简化了代码的复杂度。

以上我们知道了什么是异常,为什么会有异常,以及异常处理给我们带来的好处,那么实际使用过程中,异常又有哪些种类呢?

2.异常的种类有哪些?

我们知道没有一个完全正确的Java系统,Java程序的代码或多或少都会有BUG,而这个BUG在Java语言中用Exception对象来表示,这个BUG有一个很明显的特点,就是与Java程序紧密相连,即来自于Java程序本身;另外,我们也知道Java程序运行在JVM上,在Java程序运行期间,由于Java程序导致JVM发生错误的BUG用Error对象来表示,由于此类BUG发生在运行期且与JVM有关,所以一旦发生将不可恢复,即Java程序停止运行。在Java语言中,用Throwable对象作为Exception对象和Error对象的父类。其关系如图所示:
11
对于Java程序自身导致的BUG用Exception对象表示,Exception对象在Java程序中是可以提前捕获并处理掉的(这个功能由try-catch-finally完成),以此避免因为一个小BUG导致整个系统停止运行,毕竟系统囊括各个模块,总不可能因为你一个类型转换异常而停止运行吧,所以Java语言规定Exception对象可以捕获并处理掉

Java语言是编译型语言,Java代码编译成字节码,然后由JVM解释成目标代码由CPU执行;这里包含我们常说的两个过程,即编译时和运行时;Java语言把编译时可能产生的异常称为受检查异常把运行时可能产生的异常称为不受检查异常
受检查异常是在编译时,由编译器检测出Java程序可能会抛出的异常;怎么理解呢?举几个例子就知道了:
比如读写文件时需要打开某个路径的文件,这个时候编译器就需要你处理找不到文件的异常,

try {
	FileInputStream fis = new FileInputStream(new File(""));
} catch (FileNotFoundException e1) {
	e1.printStackTrace();
}

再比如调用当前线程的sleep()方法,编译器需要你处理线程阻塞的异常,

try {
	Thread.sleep(1000);
} catch (InterruptedException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
}

其实你不用担心受检查异常,因为受检查异常并不多,而且现在的IDE都会提示你受检查异常;真正需要注意的是不受检查异常。
不受检查异常是Java程序在其运行期间可能发生的异常,这类异常用RuntimeException对象(继承自Exception类)表示,它具有不可预估性。如我们常见的NullPointException和ClassNotFoundException等。
注意:文章最后会列出一些常见的不受检查异常

看到这里,脑子或许还是一头雾水,对于受检查异常(编译时异常)和不受检查异常(运行时异常)还是傻傻的分不清。

  • 第一,我们要明确一点,受检查异常和不受检查异常这里的“检查”是受编译器检查,最常见的受检查异常是ioException。
  • 第二,受检查异常和不受检查异常都可以被捕获,这点可以通过catch块中的对象类型显示出来。
  • 第三,没有捕获的不受检查异常如果发生了,异常将会层层往外抛出直到能够处理异常为止,如果异常没有处理,调用方法的线程将抛出异常,也意味着方法调用失败,但并不会导致程序停止。
3 自定义异常

由于Java语言本身提供的异常都是与语言紧密关联的异常(如类相关,方法调用相关),这类异常给出的信息有时候并不能满足业务系统的逻辑要求,常见的就是组件异常,针对这种情况需要我们在项目中自定义异常。自定义异常可以通过继承Exception类自定义受检查异常,通过继承RuntimeException类自定义不受检查异常。下面用例子展示两类自定义异常:

public class Test {
	public static void main(String[] args) {
		Test test = new Test();
		try {
			test.method1();
		} catch (DefinedCompileException e) {
			e.printStackTrace();
		}
		
		test.method2();
	}
	// 抛出自定义的受检查异常
	void method1() throws DefinedCompileException {
		throw new DefinedCompileException();
	}
	// 抛出自定义的不受检查异常
	void method2() {
		throw new DefinedRuntimeException();
	}
	
}
// 自定义受检查异常
class DefinedCompileException extends Exception {
	public DefinedCompileException() {}
	public DefinedCompileException(String message) {
		super(message);
	}
}
//自定义不受检查异常
class DefinedRuntimeException extends RuntimeException {
	public DefinedRuntimeException() {}
	public DefinedRuntimeException(String message) {
		super(message);
	}
}

从示例中可以看出,声明method1()方法抛出自定义的受检查异常,而声明method2()方法抛出自定义的不受检查异常,在test对象调用两个方法时,method1()方法要么捕获异常,要么再往外抛,method2()方法则无需捕获异常。
既然我们可以自定义两种异常,那么什么情况下定义受检查异常,什么情况下定义不受检查异常呢
根据笔者的项目经验,一般优先定义不受检查的异常。因为定义的异常很多是针对特定业务逻辑来的,而这些逻辑错误是无法通过编译阶段来捕获而处理,因此往往需要在运行时表现出来。
PS:注意上面关键字throws,很多面试题经常把它和Throwable类放在一起来提问

4 异常处理的逻辑

我们知道异常对象用Throwable类来表示,其中文名表示“可抛出的”,所以通常都是在方法代码中通过throw关键字往外抛出一个异常对象,上面的method1()和method2()能说明这个问题。方法内抛出一个异常对象,如果是受检查的异常对象,编译器要求你要么通过try-catch捕获处理掉异常,要么通过关键字throws往外抛出异常;如果是不受检查的异常对象,则无需处理。所以,任何异常都是先抛出然后捕获处理,如果没有捕获处理则无限抛出。

先看一下构造异常对象的过程。

method1()生成一个不带参数的DefinedCompileException对象,根据继承关系可知,最终会调用Throwable类的fillInStackTrace()方法。

public Throwable() {
    fillInStackTrace();
}

fillInStackTrace()方法会把当前线程堆栈帧的信息记录到Throwable对象中。

    public synchronized Throwable fillInStackTrace() {
        if (stackTrace != null ||
            backtrace != null /* Out of protocol state */ ) {
            fillInStackTrace(0);
            stackTrace = UNASSIGNED_STACK;
        }
        return this;
    }

fillInStackTrace(arg)是一个本地方法,返回一个Throwable对象。

private native Throwable fillInStackTrace(int dummy);

所以,在Java程序中,如果发生不受检查异常且没有捕获的情况下,会看到一系列的调用路径信息,这个就是线程的堆栈帧记录。

异常对象的捕获

Java中通过try-catch代码块捕获异常对象,这里的捕获既可以捕获受检查异常也可以捕获不受检查异常,catche代码块可以单个捕获也能多个捕获,如下面的实例:

try {
	test.method1();
} catch (DefinedCompileException e) {
	e.printStackTrace();
} catch (Exception e) {
	e.printStackTrace();
}

注意,这里只要符合其中的某一个catch代码块即停止匹配,捕获的规则是从小到大,这样便于查看具体错误原因。

异常对象的外抛

方法内抛出一个异常对象,此时可以不捕获,可以在方法上用关键字throws把异常抛出去,即把异常抛给调用此方法的方法,异常由上一层的方法决定是否捕获还是继续往外抛,直到捕获并处理。对于不受检查异常对象,会一直抛到线程上,直到该线程抛出异常并停止运行。

5 使用过程中注意事项
  • 不要捕获类似于Exception通用异常,而应该捕获特定异常;
  • 不要生吞异常
  • 不要直接打印堆栈,而是使用产品日志,详细的输出到日系系统里
  • throw earyly , catch later(尽早外抛,延迟捕获),想想为什么
  • try-catch代码块会产生额外的开销,所以尽量捕获必要的代码,即try的范围尽量精确;
  • Java每生成一个Exception对象就会生成当前栈的快照,所以避免频繁生成Exception对象,
附录:常见的异常种类
ERROR类:

java.lang.OutOfMemoryError:最可怕的错误之一,JVM内存不足导致的Error,程序直接停止运行。
java.lang.StackOverflowError:最可怕的错误之一,JVM栈溢出错误,程序直接停止运行。
java.lang.NoClassDefFoundError:未找到类定义错误。当Java虚拟机或者类装载器试图实例化某个类,而找不到该类的定义时抛出该错误。
注意:NoClassDefFoundError和ClassNotFoundException的区别。

RuntimeException类:

java.lang.ClassCastException:强制类型转换异常,一般发生在向下类型转换过程中。
java.lang.IndexOutOfBoundsException:索引越界异常。
java.lang.NullPointerException:空指针异常。
java.lang.NumberFormatException:数字格式异常。
java.lang.ArithmeticException:算术条件异常。比如:除数为零时。
java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。
java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
java.lang.InterruptedException:线程阻塞异常。

受检查异常类:

java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPATH之后找不到对应名称的class文件时,抛出该异常。
java.lang.NoSuchMethodException:找不到方法异常。
注意:上面两个很容易被理解为RuntimeException,如果你去看了源码就会知道他们是受检查的异常。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值