1、异常和错误
Java作为面向对象的语言,自然把系统发生的不正确的事件也封装成了Java对象。比如一个不存在的对象,我们却试图调用它的方法,自然是行不通的,这个不正确的事件,也就被封装成为了我们常见的NullPointerException对象。
即是说,在Java程序的运行过程中,如果发生了意外事件(发生了错误或异常),则该意外会被封装成为一个对象,并把它提交给运行时的系统,寻求相应的代码来处理。意外事件在Java中分为两类,即错误和异常;而把这个意外对象的生成和提交过程,我们称之为抛出。
在Java中:
- 错误 - 不受控的,程序无法处理的
- 异常 - 容易排查的,可以处理的
2、异常的体系
万物皆可抛,Throwable就是Java语言中所有错误和异常的父类,其两个子类:Error(错误)、Exception(异常)
Error是程序无法处理的,比如OutOfMemoryError、ThreadDeath等,出现如此情况我们往往无能为力,只能交给JVM自行处理,大多数情况下,JVM会选择终止线程(真是简单粗暴...);而Exception就是程序可以处理的异常,也是我们重点要理解的部分。
Exception的分类
在异常中,又分为两种,CheckedException(受检异常)和UncheckedExcpeiton(不受检异常),其中:
- CheckedException 要求我们必须处理(要不然为什么叫CheckedExcpetion呢),使用 try catch 语句块,否则在编译阶段就无法通过(所以称之为编译期异常)
- 如大名鼎鼎的 IOException,要求我们必须捕捉处理
- UncheckedException 发生在运行期(所以称之为运行期异常 RuntimeException),具有不确定性,主要是程序的逻辑问题所引起的,难以排查
- 如 NullPointerException(空指针异常)、IndexOutOfBoundsException(数组越界异常)
3、异常的处理
3.1 try catch
对于可能发生异常的代码,我们要使用 try 语句块来进行包裹,与 try 相呼应的还要有 catch 语句块。即 try 用来检测不安全的代码,用来发现异常,而一旦某条语句出现了异常,则从此处中止,后面的代码不再执行,而是直接跳转到异常处理的代码块中,即提到的 catch 语句块。
所以:
- try 包括需要检测的代码
- catch 发生异常时进行捕获,并进行处理
public static void main(String[] args) {
Date date = null;
try {
long time = date.getTime();
System.out.println(time);
} catch (NullPointerException e) {
System.out.println("空指针异常发生了,好像我应该做些什么");
}
}
11
1
public static void main(String[] args) {
2
Date date = null;
3
4
try {
5
long time = date.getTime();
6
System.out.println(time);
7
8
} catch (NullPointerException e) {
9
System.out.println("空指针异常发生了,好像我应该做些什么");
10
}
11
}
(注意:这里只是使用运行期异常作为示例,实际上RuntimeException是不需要在编译阶段专门进行异常处理的,即不写try catch也行)
如上例:因为 line5 试图调用空对象的一个方法,所以会发生 NullPointerException,所以 line6 会跳过不执行,而直接执行 catch 块中的内容,即 line9,打印输出 “空指针异常发生了,好像我应该做些什么”
整个过程就像打棒球,发生异常就像投手丢出了棒球,这个棒球就是异常;而捕获异常就像接住棒球。
3.2 多个catch
catch 紧跟在 try 语句之后,也就是用来进行异常处理的部分,实际上 catch 可以写多个,分别用来捕获不同类型的异常,必须从小到大(即从子类到父类)的顺序进行捕获,否则会出现编译错误,且某异常只被捕捉一次,即第一个符合的 catch 处捕获。
public static void main(String[] args) {
Date date = null;
int[] arr = new int[]{0, 1, 2};
try {
int i = arr[5];
System.out.println(i);
long time = date.getTime();
System.out.println(time);
} catch (NullPointerException e) {
System.out.println("空指针异常发生了,好像我应该做些什么");
} catch (IndexOutOfBoundsException e) {
System.out.println("数组越界了");
} catch (Exception e) {
System.out.println("发生了异常");
}
}
18
1
public static void main(String[] args) {
2
Date date = null;
3
int[] arr = new int[]{0, 1, 2};
4
5
try {
6
int i = arr[5];
7
System.out.println(i);
8
long time = date.getTime();
9
System.out.println(time);
10
11
} catch (NullPointerException e) {
12
System.out.println("空指针异常发生了,好像我应该做些什么");
13
} catch (IndexOutOfBoundsException e) {
14
System.out.println("数组越界了");
15
} catch (Exception e) {
16
System.out.println("发生了异常");
17
}
18
}
我们把之前的例子改一下,如上,按照刚才我们提到的,这里可能出现数组越界和空指针异常,所以我们写了两个catch,可能还有其他我们没想到的异常,所以我们再捕捉了一个异常的父类Exception。这个例子最后只会输出 "数组越界了",因为异常只被捕捉一次,且后面的代码不再执行。
public static void main(String[] args) {
Date date = null;
int[] arr = new int[]{0, 1, 2};
try {
int i = arr[5];
System.out.println(i);
long time = date.getTime();
System.out.println(time);
} catch (Exception e) { //错误的写法,异常捕获的顺序只能从子类到父类
System.out.println("发生了异常");
} catch (NullPointerException e) {
System.out.println("空指针异常发生了,好像我应该做些什么");
} catch (IndexOutOfBoundsException e) {
System.out.println("数组越界了");
}
}
17
1
public static void main(String[] args) {
2
Date date = null;
3
int[] arr = new int[]{0, 1, 2};
4
5
try {
6
int i = arr[5];
7
System.out.println(i);
8
long time = date.getTime();
9
System.out.println(time);
10
} catch (Exception e) { //错误的写法,异常捕获的顺序只能从子类到父类
11
System.out.println("发生了异常");
12
} catch (NullPointerException e) {
13
System.out.println("空指针异常发生了,好像我应该做些什么");
14
} catch (IndexOutOfBoundsException e) {
15
System.out.println("数组越界了");
16
}
17
}
注意catch异常的顺序,如上例中的写法是无法通过编译的,因为Exception是所有异常的父类,而catch块必须按照从子类到父类的顺序进行编写。
3.3 finally
在异常处理中,还有一个语句块叫 finally,可以不写,一旦写上,那么有且只能有一个finally语句块,该部分的代码内容总是会执行的。一般是跟在最后的catch块之后。
public static void main(String[] args) {
Date date = null;
int[] arr = new int[]{0, 1, 2};
try {
int i = arr[5];
System.out.println(i);
long time = date.getTime();
System.out.println(time);
} catch (NullPointerException e) {
System.out.println("空指针异常发生了,好像我应该做些什么");
} catch (IndexOutOfBoundsException e) {
System.out.println("数组越界了");
} catch (Exception e) {
System.out.println("发生了异常");
} finally {
System.out.println("我总是要执行的");
}
}
19
1
public static void main(String[] args) {
2
Date date = null;
3
int[] arr = new int[]{0, 1, 2};
4
5
try {
6
int i = arr[5];
7
System.out.println(i);
8
long time = date.getTime();
9
System.out.println(time);
10
} catch (NullPointerException e) {
11
System.out.println("空指针异常发生了,好像我应该做些什么");
12
} catch (IndexOutOfBoundsException e) {
13
System.out.println("数组越界了");
14
} catch (Exception e) {
15
System.out.println("发生了异常");
16
} finally {
17
System.out.println("我总是要执行的");
18
}
19
}
如上例中,除了输出 “数组越界了”,还始终会输出 “我总是要执行的”。不论程序是否发生异常,finally代码块总是会执行,所以finally一般也用来关闭资源。
需要注意的是,之前提到finally是可选的,即可以只有try和catch,同时要知道的是:
- 可以只有try和finally
- 可以只有try和catch
- 不能只有try
3.4 异常处理中的return
在Java语言的异常处理中,finally块的作用就是为了保证无论出现什么情况,finally块里的代码一定会执行。而return意味着结束了对当前函数的调用并跳出这个函数体,那么如果try块中或者catch块中出现了return,finally又如何是好?
我们看这样一道题,下面的函数最终会返回多少?
链接:https://interview.nowcoder.com/questionTerminal/5a6ea98ed42347fe81c950a1a206dc7e?toCommentId=77575
来源:牛客网
public static int func (){
try{
return 1;
}catch (Exception e){
return 2;
}finally{
return 3;
}
}
13
1
2
链接:https://interview.nowcoder.com/questionTerminal/5a6ea98ed42347fe81c950a1a206dc7e?toCommentId=77575
3
来源:牛客网
4
5
public static int func (){
6
try{
7
return 1;
8
}catch (Exception e){
9
return 2;
10
}finally{
11
return 3;
12
}
13
}
只需要记住,无论如何finally语句都要执行(除非调用了System.exit()方法)。同时,如果:
- finally中没有return,那么不会影响try或catch中return的结果,但是finally还是会执行的
- finally中有return语句,那么会覆盖掉try或catch中的return的结果,再进行返回
public static int fun() {
int i = 0;
try {
return i;
} catch (Exception e) {
return -1;
} finally {
++i;
}
}
x
1
public static int fun() {
2
int i = 0;
3
4
try {
5
return i;
6
} catch (Exception e) {
7
return -1;
8
} finally {
9
++i;
10
}
11
}
如上例,(前提是finally中没有return,否则会覆盖值)当执行到return时,结果会被保存等待finally执行完毕后返回,这个时候无论finally内部如何改变这个值,都不会影响返回结果。最终返回0。