什么是异常?
简单的一句话来说:在Java中,将程序执行过程中发生的不正常行为称为异常。
在写代码的过程中,我们经常会遇到各种各样的异常,下面就通过三个常见的异常来引出下文。
异常一:算术异常(ArithmeticException)
System.out.println(1/0);
运行结果:
异常二:数组越界异常(ArrayIndexOutOfBoundsException)
int[] array=new int[3];
array[3]=123;
运行结果:
异常三:空指针异常(NullPointerException)
int[] array=null;
System.out.println(array.length);
运行结果:
异常的分类
异常可以分为编译时异常(受查异常)和运行时异常(非受查异常),在讲异常之前,我们先对异常的上层知识进行一个简单的了解。
- Throwable:是异常体系的顶层类,它被两个子类(Error和Exception)继承
- Error:指的是Java虚拟机无法解决的严重问题(比如栈溢出或者堆溢出等),这时候就要对代码逻辑等进行一个详细的检查
- Exception:异常产生后可以通过代码来对异常进行处理,跳过这个异常,可以继续执行下面的语句
编译时异常
编译时异常也称为受查异常。
有一些异常连编译都不能够通过,更不用说运行了,类似这种异常,必须在编译的时候就将其处理掉(这种异常在IDEA编译器中都会有提示,一般是以红色的下划线为主),才能够正常运行。
class A{
public int a;
@Override
protected A clone() {
return (A)super.clone();
}
}
注意:少写了个分号、写漏了个括号……类似这些不叫编译时异常,而应该是编译时的语法错误,一般这些错误IDEA也都是会自动提醒的。
运行时异常
运行时异常也称为非受查异常。
运行时异常也是我们见得最多的异常了,例如前面开头的三个例子就都是运行时异常。
运行时异常有非常多种,但是既然是运行时才出现的问题,那么也就说明这些程序已经通过编译生成.class文件了,再由JVM执行的过程中才出现的错误。类似这种运行时的异常,IDEA一般都是不会提醒的,只有运行之后才能知道问题所在。
异常的抛出
在Java中,可以借助throw关键字来抛出一个指定的异常对象,将错误信息告知给调用者。
语法为:
throw new XXXException("异常产生的原因");
类似的例子可以看之前“Java:图书管理系统”一文中的代码,这里再重新举一个简单的例子:
public class Main {
public static void main(String[] args) {
int index=3;
int[] array=new int[index];
if(array==null){
throw new NullPointerException("传递的数组为null");
}
if(index<0||index>array.length){
throw new ArrayIndexOutOfBoundsException("数组越界");
}
}
}
注意:
- throw必须写在方法体的内部
- 抛出的对象必须是Exception或其子类对象
- 如果抛出的是RunTimeException或其子类时,可以不用进行处理,直接交给JVM来处理即可
- 异常一旦抛出,其后面的代码就不会执行
异常的捕获
异常的捕获(处理方式):一是异常声明throws,二是try - catch捕获并处理。
异常声明throws
当方法中抛出异常的时候,如果不想在这里就进行处理的话,就可以使用异常声明,借助throws将异常抛给方法的调用者,在由方法的调用者进行异常处理。
class A{
public int a;
@Override
protected A clone() throws CloneNotSupportedException {
return (A)super.clone();
}
}
(此代码就是上面的例子进行异常声明后,就不会再报错了)
注意:
- throws必须跟在方法参数的后面
- 声明的异常必须是Exception或其子类
- 如果方法的内部同时抛出了多个异常,那么throws后面就必须跟上多个异常类型,之间需要使用逗号隔开
- 如果抛出的多个异常类型具有父子类关系的,那么只要声明父类即可
- 当需要调用声明抛出异常的方法的时候,调用者需要对此异常进行处理,或者再次使用throws声明异常
try - catch捕获并处理
由于上面使用throws只是对异常进行声明,实质上并没有对异常进行真正的处理,而是直接将异常抛给方法的调用者,当调用者没有对其进行处理时,它依旧是会报错的。
如果要真正意义上对异常进行处理,不能够只是靠throws来声明,而应该使用try - catch捕获并处理异常。
语法为:
try{
//这里放入可能会出现异常的代码;
}catch (要捕获的类型 e) {
//如果try中的代码抛出异常了,并且try中抛出的异常与catch捕获是异常的类型一致(或者是其子类)的话,就会被捕获到
//然后就会执行这里面的代码
//对异常处理完成后,就会直接跳出try - catch语句,继续执行后面的代码
}
继续完善上面的例子:
class A{
public int a;
@Override
protected A clone() throws CloneNotSupportedException {
return (A)super.clone();
}
}
public class Main {
public static void main(String[] args) {
A a=new A();
try{
A b=a.clone();
} catch (CloneNotSupportedException e) {
System.out.println("异常:CloneNotSupportedException");
e.printStackTrace();
}
}
}
注意:
- try语块内抛出异常之后的代码是不会被执行的
- 异常是按照类型来进行捕获的,如果抛出的异常类型捕获的异常类型不匹配,那么就表明异常没有被成功捕获,则会继续往外抛异常,直到JVM收到后才会中断程序
- 我们一般在捕获异常的时候是通过使用多个catch来捕获的,如果多个异常的处理方法是相同的,那么也可以写成如下格式:
catch(要捕获的类型1 | 要捕获的类型2 e){
...
}
- 如果异常之间具有父子类关系,那么一定是子类的异常先排在前面进行捕获,父类的异常排在后面进行捕获(因为如果是反过来则可能会子类的异常是永远捕获不到的,造成错误)
在前面的学习,我们知道了Exception类是所有异常类的父类,那么这时候就有人有下面这个疑问了:
既然Exception类是所有异常类的父类,那么我们在catch对异常进行获取的时候,直接就写一个 catch(Exception e){…} 语句不就可以解决所有问题了?无论抛出什么异常,全部都是Exception类的子类,都是可以直接就捕获到了。
我的回答是对的。其实这段话本身就没有错误的,但是我们在实际的开发过程中是很少这样去使用的,因为一旦这样做,当捕获到异常的时候,只能够知道try语句中的这行代码是异常的,并不能确切地知道这行代码到底是因为上面而抛出异常的,这样的话,在检查代码的时候就会变得非常困难。
finally
这里补充一个finally语块,其实finally语块应该是结合在捕获并处理那里的,完整的表示应该是下面这样。
语法为:
try{
//这里放入可能会出现异常的代码;
}catch (要捕获的类型 e) {
//如果try中的代码抛出异常了,并且try中抛出的异常与catch捕获是异常的类型一致(或者是其子类)的话,就会被捕获到
//然后就会执行这里面的代码
//对异常处理完成后,就会直接跳出try - catch语句,继续执行后面的代码
}finally{
//此处的语句无论是否发生异常,都是会执行的
}
//如果没有抛出异常,否则异常已经被处理了,这里的代码也是会执行的
这时候就又有人问了:上面不是说了不管try语块中是否有异常,有的话就对异常处理完成后,就会直接跳出try - catch语句,继续执行后面的代码;没有的话就直接执行后面的代码。那么这样的话,跟finally又有什么区别呢?为什么还要引出finally语句这个东西呢?
这里我的回答是:写在外面不一定能够执行到,但是写在finally里面的代码是一定会被执行的。下面以一段代码为例就可以解释清楚了:
public class Main {
public static int func(){
int[] array=new int[3];
try{
array[2]=10;
return array[2];
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("ArrayIndexOutOfBoundsException");
e.printStackTrace();
}finally {
System.out.println("abcd");
}
array[0]=20;
return array[0];
}
public static void main(String[] args) {
int num=func();
System.out.println(num);
}
}
运行结果:
这段代码中try语块内部并没有存在异常的地方,所以会走到return的地方,既然是return了,自然也就不会再执行后面的代码了,但是在运行结果中可以看到是有“abcd”打印出来的,这就说明了:无论如何,finally语块中的代码是一定会被执行的。
注意:虽然finally语块中也是可以写return的,由于finally语块相对于try语块中的return,一般来说是优先执行的,所以返回的值也应该是finally语块中return返回的值。但是我们一般是不建议在finally语句中写return的(编译器可能会将其当做一个警告)。
补充:throw和throws的区别
throw:用于抛出异常,throw new ……
throws:用在方法上来进行异常声明的
总结
异常处理的流程:
- 程序向执行try语块中的代码
- 如果try中代码出现异常,则会结束try语块中的代码,检验与catch中的异常类型是否匹配
- 如果找到匹配的异常类型,就会执行catch中的代码
- 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者
- 无论是否找到匹配的异常类型,finally中的代码都是会被执行的
- 如果上层调用者也没有处理异常,那么将会继续向上传递,一直到main方法也没有找到合适的代码来处理这个异常,此时就会交给JVM进行处理,整个程序也随之异常终止(只要是交给JVM处理的,都是属于异常终止)
自定义异常类
自定义异常类很多时候是开发中使用得最多的。
具体的实现方式是:
- 先自定义一个异常类,让其继承Exception或者RunTimeException
- 实现一个带有String参数类型的构造方法,其中参数是用来写初夏异常的原因
代码示范:
class A extends Exception {
public A(String message) {
super(message);
}
}
注意:
- 自定义异常都是会继承Exception类或者是RunTimeException类的
- 自定义异常继承Exception类默认是受查异常的(也就是编译时异常)
- 自定义异常继承RunTimeException类默认是非受查异常(也就是运行时异常)