1.初见 Java 异常
1.1 熟悉的面孔
在 Java 的学习过程中,一定遇到过除数为 0,数组越界,对空引用操作等,这些情况可以通过编译,但是在运行的时候编译器会报出异常,这些异常就是 Java 异常机制中的一部分。
System.out.println(10 / 0);//除数为0
复制代码
int[] arr = new int[10];
System.out.println(arr[11]);
复制代码
String str = null;
System.out.println(str.length());
复制代码
ArithmeticException,ArrayIndexOutOfBoundsException,NullPointerException都是初学 Java 时常遇到的一部分异常,但是 Java 异常机制包括的不仅仅只有异常,还有错误也是属于 Java 异常机制范围内的。
1.2 防御式编程
错误在代码中是客观存在的. 因此我们要让程序出现问题的时候及时通知程序猿, 我们有两种主要的方式:LBYL: Look Before You Leap. 在操作之前就做充分的检查.EAFP: It's Easier to Ask Forgiveness than Permission. "事后获取原谅比事前获取许可更容易". 也就是先操作, 遇到问题再处理.
打个比方,有一天你心血来潮,看到一位非常漂亮的小姐姐,你很想牵她的手,但是你非常的有礼貌,先询问这位小姐姐:我能牵你的手吗?,这位小姐姐毫不犹豫地说:不行!,这一种情形就是 LBYL 方式;与此同时,另一位比较强悍的兄弟也想牵这位小姐姐的手,于是直接拿起这位小姐姐的手,牵了起来,然后这位小兄弟被扇了一巴掌,这种情形就是 EAFP 方式。Java 异常核心思想就是 EAFP。
1.3 异常的优点
使用异常能够更好地处理业务,代码也更有条理,更加清晰明白,不使用异常正常的代码与异常的代码全部混在一起,比较混乱,后期找 bug 也不好找。比如一个模拟实现一个游戏开始的过程:
不使用异常:
public class Test2 {
public static void main(String[] args) {
boolean ret = false;//账号密码匹配结果
ret = 密码验证结果();
if (!ret) {
//处理游戏登录失败业务
}
ret = 加载游戏结果();
if (!ret) {
//处理加载游戏失败业务
}
ret = 联机匹配结果();
if (!ret) {
//处理联机匹配失败业务
}
ret = 氪金是否成功结果();
if (!ret) {
//处理氪金失败业务
}
}
}
复制代码
使用异常:
public class Test2 {
public static void main(String[] args) {
try {
密码验证();
加载游戏();
联机匹配();
氪金();
}catch (密码验证异常) {
//处理密码错误异常
}catch (加载游戏异常) {
//处理加载游戏异常
}catch (联机匹配异常) {
//处理联机匹配异常
}catch (氪金异常) {
//处理氪金异常
}
}
}
复制代码
2.异常处理
2.1 捕获异常
想要捕获一个异常,前提需要知道可能会发生什么类型的异常,最常见的异常就是前面所举例的ArithmeticException,ArrayIndexOutOfBoundsException,NullPointerException分别为除数为 0 异常,数组越界异常,空指针异常。当然,Java 异常体系中的异常种类非常多,Exception 类下的直接子类就有:
RuntimeException 类是目前我们接触最多的,前面列举的几个常见的异常都是该类的子类。
2.1.1 捕获一个异常
我们在进行除法计算时,可能会出现除数为 0,访问数组对象时,可能出现数组越界,使用一个对象时,可能会出现空指针异常,知道一段代码可能出现某种或某些异常,我们可以对异常进行捕获,语法规则为:
try {
//可能出现异常的代码段
}catch (异常类 异常变量名) {
//处理该异常
}
复制代码
try
代码块中放的是可能出现异常的代码。catch
代码块中放的是出现异常后的处理行为。
public class Test3 {
public static void main(String[] args) {
int a = 0;
try {
System.out.println(8 / a);
}catch (ArithmeticException e) {
System.out.println("捕捉到了除数为0异常!");
}
int[] arr = new int[10];
try {
System.out.println(arr[20]);
}catch (ArrayIndexOutOfBoundsException e) {
System.out.println("捕捉到了一个数组越界异常!");
}
String str = null;
try {
System.out.println(str.equals("aaa"));
}catch (NullPointerException e) {
System.out.println("捕捉到了一个空指针异常!");
}
}
}
复制代码
可以调用异常类中的printStackTrace()方法打印异常错误信息:
public class Test3 {
public static void main(String[] args) {
int a = 0;
try {
System.out.println(8 / a);
}catch (ArithmeticException e) {
e.printStackTrace();
System.out.println("捕捉到了除数为0异常!");
}
int[] arr = new int[10];
try {
System.out.println(arr[20]);
}catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕捉到了一个数组越界异常!");
}
String str = null;
try {
System.out.println(str.equals("aaa"));
}catch (NullPointerException e) {
e.printStackTrace();
System.out.println("捕捉到了一个空指针异常!");
}
}
}
复制代码
如果异常没有被主动捕捉(使用 try...catch 语句捕捉),这个异常会交给 JVM 处理,一旦交给 JVM 处理,发生异常,将非正常终止程序,后续代码不会执行。
public class Test6 {
public static void main(String[] args) {
int[] arr = new int[10];
System.out.println(arr[20]);//发生数组越界异常,该异常没有被捕获,将交给JVM处理
System.out.println("我是异常代码的后续代码,我被执行了!");
}
}
复制代码
代码非正常退出,异常处后的代码不会被执行!
主动捕获异常,异常处后面的代码会执行,并且程序正常退出:
public class Test5 {
public static void main(String[] args) {
int[] arr = new int[10];
try {
System.out.println(arr[20]);
}catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕捉到了一个数组越界异常!");
}
System.out.println("我是异常代码的后续代码,我被执行了!");
}
}
复制代码
2.1.2 捕获多种异常
Java 异常机制支持多种异常的捕获,最常用的方法,就是使用多个 catch 语句,捕捉多个异常的具体类型。一个 try...catch 语句,catch 语句可以存在多个:
public class Test4 {
public static void main(String[] args) {
try {
//可能出现异常的代码段
}catch (异常类 异常变量名) {
//处理该异常
}catch (...) {
//处理
}...
}
}
复制代码
而对于 try 语句中的代码段,当代码段中遇到异常时,发生异常的后面的所有代码段将不再执行,会直接执行对应 catch 中的语句。比如,我将三句都有异常的代码写在同一个 try 语句中,使用多个 catch 语句捕捉这三种类型的异常,实际上只会捕捉到第一个异常。
public class Test7 {
public static void main(String[] args) {
int a = 0;
int[] arr = new int[10];
String str = null;
try {
System.out.println(8 / a);
System.out.println(arr[20]);
System.out.println(str.equals("aaa"));
} catch (ArithmeticException e) {
e.printStackTrace();
System.out.println("捕捉到了除数为0异常!");
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕捉到了一个数组越界异常!");
} catch (NullPointerException e) {
e.printStackTrace();
System.out.println("捕捉到了一个空指针异常!");
}
}
}
复制代码
try 语句中第一个异常语句为除数为 0 而引发的算术异常,所以最终捕捉的异常只有算术异常。如果第一个语句没有异常,则捕捉的是后面第一个发生异常语句的异常,也就是说一个 try 语句只能捕获一个异常。
public class Test7 {
public static void main(String[] args) {
int a = 2;
int[] arr = new int[10];
String str = null;
try {
System.out.println(8 / a);
System.out.println(arr[20]);
System.out.println(str.equals("aaa"));
} catch (ArithmeticException e) {
e.printStackTrace();
System.out.println("捕捉到了除数为0异常!");
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕捉到了一个数组越界异常!");
} catch (NullPointerException e) {
e.printStackTrace();
System.out.println("捕捉到了一个空指针异常!");
}
}
}
复制代码
除了使用多个 catch 语句,还可以使用|
分隔符在一个 catch 语句中定义多种类型的异常类:
public class Test8 {
public static void main(String[] args) {
int[] arr = new int[10];
String str = null;
try {
System.out.println(2/0);
} catch (ArrayIndexOutOfBoundsException | NullPointerException | ArithmeticException e) {
e.printStackTrace();
System.out.println("捕捉到了一个除数为0异常或数组越界异常或空指针异常!");
}
}
}
复制代码
public class Test8 {
public static void main(String[] args) {
int[] arr = new int[10];
String str = null;
try {
System.out.println(arr[20]);
System.out.println(str.equals("aaa"));
} catch (ArrayIndexOutOfBoundsException | NullPointerException | ArithmeticException e) {
e.printStackTrace();
System.out.println("捕捉到了一个除数为0异常或数组越界异常或空指针异常!");
}
}
}
复制代码
public class Test8 {
public static void main(String[] args) {
int[] arr = new int[10];
String str = null;
try {
System.out.println(str.equals("aaa"));
} catch (ArrayIndexOutOfBoundsException | NullPointerException | ArithmeticException e) {
e.printStackTrace();
System.out.println("捕捉到了一个除数为0异常或数组越界异常或空指针异常!");
}
}
}
复制代码
也可以直接使用这些类的父类RuntimeExcept
或父类的父类Exception
处理其所有子类类型的异常,但是不推荐,因为范围太广了,如果没有printStackTrace()方法,根本不知道程序发生了什么异常。
public class Test8 {
public static void main(String[] args) {
int[] arr = new int[10];
String str = null;
try {
System.out.println(str.equals("aaa"));
} catch (RuntimeException e) {
e.printStackTrace();
System.out.println("捕捉到了一个运行时异常!");
}
}
}
复制代码
最后,还有一点需要注意,就是使用多个 catch 语句时,子类异常要在父类异常前面,否则编译器会报错!这一点其实很好理解,正所谓“小的打不过,老的再来!”。
我把NullPointerException
定义在其父类RuntimeException
后面,编译器报错了,我换一下编译器就没有报错了,程序也能够正常运行。
所以,catch
语句捕获异常时,捕获异常的顺序应该是:
2.2finally
在try...catch
语句的基础之上,后面还可以加上finally
语句,无论是否发生异常,异常是否被捕获,该语句里面的代码块都会被执行,一般用于资源的关闭,比如常见的Scanner
类,使用该类进行输入是需要进行资源关闭的。语法规则:
try {
//可能出现异常的代码段
}catch (异常类 异常变量名) {
//处理该异常
}catch (...) {
//处理
}finally {
//处理,不论异常是否捕获,异常是否交给JVM处理,该语句都会执行
}
复制代码
finally
代码块中的代码用于处理善后工作, 会在最后执行。
import java.util.InputMismatchException;
import java.util.Scanner;
public class Test10 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try {
int a = sc.nextInt();
System.out.println(10 / a);
} catch (InputMismatchException e) {
e.printStackTrace();
System.out.println("输入数据类型错误!");
} catch (ArithmeticException e) {
e.printStackTrace();
System.out.println("算术异常,除数为0!");
}
sc.close();//关闭资源
}
}
复制代码
因为 sc.close()
语句不论什么情况都要执行,所以该代码可以改成:
import java.util.InputMismatchException;
import java.util.Scanner;
public class Test11 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try {
int a = sc.nextInt();
System.out.println(10 / a);
} catch (InputMismatchException e) {
e.printStackTrace();
System.out.println("输入数据类型错误!");
} catch (ArithmeticException e) {
e.printStackTrace();
System.out.println("算术异常,除数为0!");
} finally {
sc.close();//关闭资源
}
}
}
复制代码
刚才的代码可以有一种等价写法, 将 Scanner 对象在 try 的 ( ) 中创建, 就能保证在 try 执行完毕后自动调用 Scanner 的 close 方法,上面的代码也等价于:
import java.util.InputMismatchException;
import java.util.Scanner;
public class Test11 {
public static void main(String[] args) {
try (Scanner sc = new Scanner(System.in)) {
int a = sc.nextInt();
System.out.println(10 / a);
} catch (InputMismatchException e) {
e.printStackTrace();
System.out.println("输入数据类型错误!");
} catch (ArithmeticException e) {
e.printStackTrace();
System.out.println("算术异常,除数为0!");
}
}
}
复制代码
使用 finally 的弊端:
public class Test12 {
public static int func() {
try {
return 10;
}finally {
return 20;
}
}
public static void main(String[] args) {
System.out.println(func());
}
}
复制代码
所以使用finally
时,要避免在finally
语句中进行返回值返回。
2.3Java 异常体系
Java 内置了丰富的异常体系, 用来表示不同情况下的异常。
[x] 顶层类 Throwable
派生出两个重要的子类,Error
和 Exception
。
[x] Error
指的是 Java 运行时内部错误和资源耗尽错误. 应用程序不抛出此类异常. 这种内部错误一旦出现,除了告知用户并使程序终止之外, 再无能无力, 如递归时造成的栈溢出,堆溢出。
[x] Exception
是我们程序猿所使用的异常类的父类,它的直接子类有很多,图中只展示了 3 种。
[x] Exception
有一个子类称为 RuntimeException
, 这里面又派生出很多我们常见的异常类NullPointerException
, IndexOutOfBoundsException
等。
[x] Error
类与 RuntimeException
类,为非受查异常,其他异常类为受查异常。
受查异常必须使用try...catch
语句进行处理,否则报错,非受查异常可以使用try...catch
语句,也可以不使用。
异常的种类有很多, 我们要根据不同的业务场景来决定。对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果。对于不太严重的问题(大多数场景), 可以记录错误日志, 并通过监控报警程序及时通知程序猿。对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试。在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息。
2.4 异常处理的注意事项
2.4.1 异常捕捉时的注意事项
[x] 程序发生异常未被try
捕获处理,将会交给 JVM 处理,一旦交给 JVM 处理,程序直接非正常终止。
[x] catch
语句捕获异常的顺序,从子类到父类,但是最好使用具体异常类处理。
[x] 一旦 try
中出现异常, 那么 try
代码块中的程序就不会继续执行, 而是交给 catch
中的代码来执行,catch
执行完毕会继续往下执行程序后续代码。
[x] finally
里面的代码,无论异常是否被捕获,都会执行。
[x] 由于 Exception
类是所有异常类的父类, 因此可以用这个类型表示捕捉所有类型的异常。
[x] try
语句需要搭配catch
或finally
使用。
2.4.2 异常处理的流程
程序先执行 try 中的代码。
如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配。
如果找到匹配的异常类型, 就会执行 catch 中的代码。
如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者。
无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行)。
如果上层调用者也没有处理的了异常, 就继续向上传递,一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止。
2.5 手动抛出异常
除了 Java 内置的类会抛出一些异常之外, 程序猿也可以手动抛出某个异常,使用 throw
关键字完成这个操作。
public class Test13 {
public static int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("除0异常!");
}
return a / b;
}
public static void main(String[] args) {
System.out.println(divide(12, 0));
}
}
复制代码
我们在处理异常的时候, 通常希望知道这段代码中究竟会出现哪些可能的异常,我们可以使用 throws
关键字, 把可能抛出的异常显式的标注在方法定义的位置, 从而提醒调用者要注意捕获这些异常,相当于声明效果。
public class Test14 {
public static int divide(int a, int b) throws ArithmeticException{
if (b == 0) {
throw new ArithmeticException("除0异常!");
}
return a / b;
}
public static void main(String[] args) {
try {
System.out.println(divide(12, 0));
} catch (ArithmeticException e) {
e.printStackTrace();
}
}
}
复制代码
3.自定义异常
Java 内置了很多的异常,但是总会有需求在内置的异常中找不到合适的,这个时候就需要自己定义一个合适的异常,来应对需求。我们以一个用户登录来介绍自定义异常的使用,在此之前,需要知道:
自定义的异常需要继承 Java 内置的异常类,通常会继承自 Exception
或者 RuntimeException
,继承Exception
表示自定义的异常时受查异常,继承 RuntimeException
表示自定义的异常是非受查异常。
用户进行输入账号密码登录时,可能会出现账号不存在或者密码错误的情况,这两种情况是异常情况,需要进行处理,所以可以自定义两个类NameException
和PasswordException
,前者表示账号不存在异常,后者表示密码错误异常,由于该异常是在用户输入时才会出现,所以不妨继承RuntimeException
,定义为非受查异常。
class NameException extends RuntimeException {
public NameException(String exceptionMessage) {
super(exceptionMessage);
}
}
class PasswordException extends RuntimeException {
public PasswordException(String exceptionMessage) {
super(exceptionMessage);
}
}
复制代码
模拟用户登录:
public class Test15 {
private static String userName = "未见花闻";
private static String password = "5201314";
public static void login(String userName, String password) throws NameException, PasswordException{
if (!Test15.userName.equals(userName)) {
throw new NameException("用户名不存在!");
}
if (!Test15.password.equals(password)) {
throw new PasswordException("密码错误!");
}
System.out.println("登陆成功");
}
public static void main(String[] args) {
try {
login("csdn", "123456");
}catch (NameException | PasswordException e) {
e.printStackTrace();
}
try {
login("未见花闻", "123456");
}catch (NameException | PasswordException e) {
e.printStackTrace();
}
System.out.println("============");
try {
login("未见花闻", "5201314");
}catch (NameException | PasswordException e) {
e.printStackTrace();
}
}
}
复制代码
评论