1、简单认识异常
在我们程序执行的过程中,发生不正常的行为称为异常,就比如我们常见的数组下标越界,对空引用进行使用也就是空指针异常,再者还碰到过除数为 0 时发生的算数异常。
算数异常:
System.out.println(5/0); //Exception in thread "main" java.lang.ArithmeticException: / by zero
数据越界异常:
int[] array = { 1,2,3,4,5 }; System.out.println(array[10]); //Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10
空指针异常:
int[] array = null; System.out.println(array.length); //Exception in thread "main" java.lang.NullPointerException
通过观察//我们上面几个常见的异常例子,可以通过编译器输出的信息,观察到每个异常都有与其对应的类来进行描述,比如果空指针异常,描述他的类就是在 java.lang 包里面的NullPointerException类里面!
2、异常的体系结构
异常种类是非常多的,上述我们只是举例我们目前阶段常见的几个异常,为了对不同异常或错误进行更好的分类和管理,Java内部维护了一个异常的体系结构:
像最右边的运行时异常,还有很多子类,比如上面举例的数组越界啊等等,最左边的IO异常也有很多子类,这里就不一一列举了。
从上面的图我们也可也分析到:Throwable 是异常体系的顶层类,派生出两个重要的子类,分别是Error 和 Exception。
Error:指的是Java虚拟机无法解决的严重问题,比如栈溢出,堆资源耗尽,JVM内部错误等,一旦发生了就没有回天之术了!
Exception:这个指的是产生了异常之后,程序员可以通过对代码进行处理,使程序继续执行,好比如说我除数写成 0 了,我发现异常了,我修改即可,我们平时说的异常就是Exception!
今天我们不讨论Error主要讨论Exception
3、异常的分类
对于Java中的异常,可能在程序编译时发生,也可能在程序运行时发生,所以根据他们发生的时机不同,我们可以讲异常分为:编译时异常和运行时异常:
3.1 编译时异常
在程序编译的期间发生的异常,称为编译时异常,也可以称为受检查异常(CheckedException),此时如果程序想要编译,必须处理掉这个异常!
比如:我们写一个Student类,里面重写 Object 类中的 clone 方法:
class Student { private String name = "zhangsan"; private int age = 14; @Override protected Object clone() throws CloneNotSupportedException { return (Student)super.clone(); } }
这里 throws 关键字先不用管,他是声明异常的,就是声明这个 clone 这个方法可能会发生这个异常,但不进行处理,交给调用者处理,后面会讲。
首先重写这个方法肯定是没有任何问题的,那我们现在来看到在main函数中调用这个方法会出现什么问题:
这里显示有一个未处理的异常,因为 Object 类中 clone 方法抛出了 CloneNotSupportedException异常,而这个异常类继承了 Exception 异常类,默认是编译时异常!这些我们后面都会学习!解决方法你可以接着声明这个异常交给JVM处理,等学完后面的内容就明白了。
3.2 运行时异常
在程序执行期间发生的异常,称为运行时异常,也可也称为非受检查异常(UncheckedException),在 RuntimeException 以及其子类对应的异常,都称之为运行时异常,比如前面的空指针异常,数组越界异常,运行时异常编译是可以正常通过的!比如:
这里就是在运行程序的时候发生的异常,也就是运行时异常!
注意:
如果编译时出现的是语法错误,不能把它称之为异常,比如把 System.out.println 的大写 S 写成了小写 s ,这种在编译过程中就会出错,是在"编译期"出错,而运行时异常指的是程序已经编译通过得到了对应的字节码文件,在由JVM执行过程中出现的错误!
4、如何处理异常?
在Java中,处理异常主要有这五个关键字:thorw 抛出异常,try 里面放可能出现的异常代码,catch捕捉异常,finally 必须执行的特定代码,thorws 声明异常。
4.1 异常的抛出
前面都是Java自己的异常,编译器帮我们输出的,那其实我们也可以自己手动抛出异常,也可也自定义异常(后面讲),那如何抛出异常呢?可以借助 thorw 关键字,抛出一个指定的异常对象,将错误信息告诉给调用者。
语法格式:throw new xxxException("出现异常的原因");
比如说我们要写一个方法,来获取数组对应下标的值,这里当用户传递的数组为null,我们就可以手动抛出异常,输入的下标越界了,我们也可以手动抛出异常:
public class TestDemo { public static int getArrayValue (int[] array, int index) { if (array == null) { throw new NullPointerException("getArrayValue::传递的数组为空!"); } if (index < 0 || index >= array.length) { throw new ArrayIndexOutOfBoundsException("getArrayValue::数组下标越界了!"); } System.out.println("测试抛出异常后会不会执行后续代码!"); return array[index]; } public static void main(String[] args) { int[] array = {1,2,3,4,5}; int ret = getArrayValue(array, 10); System.out.println(ret); } }
当我们运行这个 main 方法,手动抛出异常和不手动抛出有什么区别呢?
这里区别就交给小伙伴自己去体会了!其实没啥区别,你 new 对象时构造方法不传要打印的错误信息其实就跟编译器自己抛出的异常几乎是一样的了。
这里我们手动抛异常需要注意几个点:
- throw关键字必须写在方法体的内部
- 抛出的异常必须是 Exception 或者 Exception 的子类对象
- 如果抛出的是 RuntimeException 或者 RuntimeException 的子类,也就是抛出运行时异常,则可以不用处理,交给编译器处理!
- 如果抛出的是编译时异常,则必须处理了才能进行编译
- 异常一旦抛出了,其后面的代码将不会执行!(上述代码执行的例子也能看出来)
4.2 异常的捕获
既然我们抛出了异常,而且只要异常抛出了,就不会执行后面的代码,那要是我们仍然想执行后面的代码怎么办?就比如你没有手机卡就不能打电话,但是你仍然可以打开打电话的App!不能说我只要发现了一个异常,我程序就直接终止了!
4.2.1 throws 关键字
在捕获异常之前,我们要先来学习下 thows 关键字,也就是异常的声明:
thows关键字是写在方法声明时参数列表的后面,当方法中抛出编译时异常,如果该方法内部不想处理该异常,就可以借助 throws 关键字声明可能出现这个异常,但是并不进行处理,而是将异常抛给方法的调用者,提醒方法的调用者来处理这个异常。
语法格式:
修饰符返回值类型方法名(参数列表) throws 异常类型1,异常类型2...{
}
假设我们有一个方法,里面可能会发生异常,但是我们不想处理这个异常,那么就可以声明这个异常,那交给谁处理呢?谁调用了这个方法谁处理,假设是 main 方法调用了,就由 main 方法处理,如果 main 方法也不想处理,也可以使用 throws 声明这个异常,即交给JVM处理,如何自己处理异常,后面的 try - catch 就会讲到!
注意点:
- throws 必须跟在方法的参数列表后面,声明的异常必须是 Exception 或者 Exception 子类!
- 如果抛出了多个异常,throws 必须跟多个异常类型,之间使用逗号进行隔开,如果抛出的异常有父子关系,可以直接声明父类(不推荐!)