什么是异常
异常就是在代码运行的时候,程序出现的问题。
我们来看个例子:
int[] arr = {1, 2, 3};
System.out.println(arr[3]);

报错:ArrayIndexOutOfBoundsException
Java中异常的分类
Java.lang.Throwable分为两类,Error和Exception。
Error表示系统级别的错误,是Java开发公司的使用的。
Exception称之为异常,代表程序可能出现的问题,当程序出现异常的时候,通常使用Exception及其子类封装程序出现的异常。Exception分为两类RuntimeException和其他异常(编译时异常)
RuntimeException及其子类,称之为运行时异常,在编译中不会抛出异常,但在运行时会抛出异常。如上的例子中,数组索引越界就是运行时异常。
编译时异常是在编译阶段就会抛出的异常,在Exception中,除了RuntimeException及其子类,都是编译时异常,必须要手动修改才可以正常编译成字节码文件。

在代码中处理异常
JVM默认的处理异常的方式
JVM在遇到异常的时候,会:
- 把异常的原因,异常出现的位置展示在控制台;
- 程序运行到出现异常的位置后停止执行(也就是运行到异常存在的语句,异常存在的语句之后的语句全部不执行)
我们可以举例:double num = 1/0;这一句是算数异常:ArithmeticException,属于运行时异常。
System.out.println("Hello World.");
double num = 1/0;
System.out.println("I am a cat.");
当语句中夹杂一个错误运行时异常时,运行情况如下:

可以看到在运行结果中,System.out.println("Hello World.");语句是正常执行的(控制台上打印了这行语句),之后再打印异常的原因,异常出现的位置。打印完异常的信息之后直接退出程序。
在代码中处理(捕获异常)
使用try(用于捕获可能出现异常的代码)、catch(如果出现异常该如何处理)、finally(无论是否发生异常都会执行的代码块,常用于资源清理);需要注意的是,无论代码是否报错,**finally**代码块中的逻辑一定会被执行。
public class YiChang {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
try{
for(int i=0;i<5;i++){
System.out.println(arr[i]);
}
} catch(Exception e){
System.out.println(e);
}
System.out.println("End.");
}
}
如上代码中,创建了一个只有三个元素的数组,之后使用一个for循环打印,但是循环重复5次,这一定会出现数组越界访问的情况,所以我们将for循环代码用try来捕获,然后如果捕获了异常则打印异常,然后程序会自动继续执行。

try、catch、finally 的语法分别是:
try{<可能会抛出错误的代码>}、catch(<报错的类型> e){<处理报错的逻辑>}、inally{<一定会执行的逻辑>}
捕获异常的好处:让程序继续按照逻辑运行,提升代码的容错度。
public class YiChang {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
try{
System.out.println(arr[10]);
} catch(ArrayIndexOutOfBoundsException e){
System.out.println("Error:ArrayIndexOutOfBoundsException");
} finally {
System.out.println("finally");
}
System.out.println("End.");
}
}
如上代码中,当代码运行到System.out.println(arr[10]);时,JVM会创建一个ArrayIndexOutOfBoundsException对象。再把ArrayIndexOutOfBoundsException对象与catch语句中设置的报错对象相对比,检查catch是否能够接收相关异常,如果能接收,则执行能接收对应异常的catch中代码块的逻辑;执行完毕之后,再执行finally语句中代码块的逻辑。
注意点1
在try语句的代码块中,如果报错,则不会继续执行接下来的逻辑,如下:
public class YiChang {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
try{
System.out.println("这是在异常之前执行的逻辑");
System.out.println("这是异常语句" + arr[10]);
System.out.println("这是在异常之后存在的逻辑");
} catch(ArrayIndexOutOfBoundsException e){
System.out.println("这是Catch语句代码块内的语句");
}
}
}

注意点2
在try中抛出的错误与catch接收的报错不一致时,也就是未被catch,异常会向上一级调用栈传播(但会执行finally中的语句)并抛出报错:
public class YiChang {
public static void main(String[] args) {
try{
System.out.println("这是在异常之前执行的逻辑");
System.out.println("这是异常语句" + 1/0);
System.out.println("这是在异常之后存在的逻辑");
} catch(ArrayIndexOutOfBoundsException e){
System.out.println("这是Catch ArrayIndexOutOfBoundsException语句代码块内的语句");
} finally {
System.out.println("这是finally中的逻辑");
}
}
}

注意点3
如果try中的代码没有抛出异常,则代码会完整执行try中的代码,执行完毕try中的代码会执行finally中的代码。由于未抛出异常,所以不会执行catch中的逻辑。
public class YiChang {
public static void main(String[] args) {
try{
System.out.println("这是在异常之前执行的逻辑");
System.out.println("这是预期异常语句" + 1/1);
System.out.println("这是在异常之后存在的逻辑");
} catch(Exception e){
System.out.println("这是Catch语句代码块内的语句");
} finally {
System.out.println("这是finally中的逻辑");
}
}
}

注意点4
如果try中的代码被多个catch捕获,如果存在多个catch,应该按照从具体到一般的原则排列,如果多个catch块的顺序不正确(即大类在前,小类在后),会导致编译错误。
public class YiChang {
public static void main(String[] args) {
try{
System.out.println("这是在异常之前执行的逻辑");
System.out.println("这是预期异常语句" + 1/0);
System.out.println("这是在异常之后存在的逻辑");
} catch(ArithmeticException e) {
System.out.println("这是Catch ArithmeticException语句代码块内的语句");
} catch(Exception e) {
System.out.println("这是Catch Exception语句代码块内的语句");
} finally {
System.out.println("这是finally中的逻辑");
}
}
}

说明
在JDK7+版本,如果存在多个catch捕获多个错误,但是错误处理是一样的,则可以|来列举可能出现的错误,官方称呼为多重捕获(multi-catch):
比如:catch(ArithmeticException | ArrayIndexOutOfBoundsException e) {}、catch (ArithmeticException | ArrayIndexOutOfBoundsException | NullPointerException e) {}
抛出异常
使用throws和throw
throws
用途:写在方法定义处,用于声明一个异常,告知调用本方法可能会出现哪些异常。throws中,如果是编译时异常,必须要声明异常,如果是运行时异常,可以选择性的不写。
public void method() throws ArithmeticException, ArrayIndexOutOfBoundsException {
// 方法的逻辑
}
throw
用途:写在方法内,用于结束方法,手动抛出异常对象,抛出后后续逻辑不再执行。
public void method() {
// 方法的逻辑
throw new ArithmeticException;
}
举例:定义一个方法,求数组中的最大的元素。
public class YiChang {
public static void main(String[] args) {
int[] arr1 = new int[0];
try {
System.out.println(getArrMax(arr1));
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
}
static int getArrMax(int[] arr)throws NullPointerException, ArrayIndexOutOfBoundsException {
if (arr==null) {
throw new NullPointerException();
}
if (arr.length==0) {
throw new ArrayIndexOutOfBoundsException();
}
int max = arr[0];
for(int i=0;i<arr.length;i++){
if(arr[i]>max){
max = arr[i];
}
}
return max;
}
}
在如上的代码中,方法getArrMax会判断是否存在传入为空指针或者数组长度为0,如果存在则使用throw抛出错误,且在方法定义中声明了可能会出现的异常。
如果出现异常,在调用处也需要使用try来捕获异常,否则会以1结束进程,报错退出程序。
自定义异常
- 定义异常类
- 写继承关系,如果是运行时异常,则继承
RuntimeException,如果是编译时异常,则继承Exception - 写空参构造
- 写有参构造
自定义异常的意义:就是让控制台报错的信息更清楚、更加符合报错的内容。说的直白一点,自定义异常就是自定义了类名,为了异常的名字。
编译时异常:(继承Exception)
public class DIYExceptionDemo extends Exception {
public DIYExceptionDemo() {}
public DIYExceptionDemo(String message) {
super(message);
}
}
运行时异常:(继承RuntimeException)
public class DIYRuntimeExceptionDemo extends RuntimeException {
public DIYRuntimeExceptionDemo() {}
public DIYRuntimeExceptionDemo(String message) {
super(message);
}
}