前言:在前面学习了那么多Java的基础知识外,我们在敲代码的时候避不开的有一个东西,那就是异常。所以本篇我们就来学习一下异常,看看Java中有什么异常,有什么用,又是从哪来的。
每文一图:

一.异常的背景
1.初识异常
首先我们先来看看我们之前遇到过的一些异常,让大家回忆一下异常这个东西:
比如:
(1).除以 0
public static void main(String[] args) {
System.out.println(10/0);
//0不能作为除数
}
运行结果报异常:
(2).访问null对象
public static void main(String[] args) {
String str = null;
System.out.println(str.length());
//str指向null 没有长度可言
}
运行结果报异常:
所以所谓异常---------->
所谓异常指的就是程序在 运行时 出现错误时通知调用者的一种机制,关键字是 “运行时”,有些错误例如将
System.out.println
拼写错了, 写成system.out.println
此时编译过程中就会出错,这是"编译期" 出错。
2.防御式编程
然后我们需要知道:错误在代码中是客观存在的! 再好的软件再好的代码,它里面也会有BUG的,所以我们要客观的面对这些BUG。因此我们要让程序出现问题的时候及时通知程序猿. 我们有两种主要的方式:
1.LBYL: Look Before You Leap
. 在操作之前就做充分的检查。
2.EAFP: It's Easier to Ask Forgiveness than Permission
。“事后获取原谅比事前获取许可更容易”。也就是先操作,遇到问题再处理。
这两个点看起来有点不好理解,但我们举个例子就通俗易懂了。就比如在男生刚开始谈恋爱的时候,在和女生逛街时想牵手,第一种就是我们的LBYL,操作前做准备,问女生我可以牵你的手吗?这种明显有些微妙;而第二种就是主动牵手,然后顺理成章一起牵着,或者是顶多被女生骂骂,是遇到问题再处理。
所以异常的核心思想就是
EAFP
!
我们再举一个游戏的例子:
下午窝在宿舍无聊,打开手机撮一把王者,(我们用伪代码演示一下开始一局王者荣耀的过程),如果是LBYL风格的代码(不使用异常):
boolean ret = false;
ret = 登陆游戏();
if (!ret) {
处理登陆游戏错误;
return;
}
ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
}
ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
}
ret = 选择英雄();
if (!ret) {
处理选择英雄错误;
return;
}
ret = 载入游戏画面();
if (!ret) {
处理载入游戏错误;
return;
}
......
也就是一种走一步测一步的这种方式,我们再看看 EAFP 风格的代码(使用异常):
try {
登陆游戏();
开始匹配();
游戏确认();
选择英雄();
载入游戏画面();
...
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} catch (选择英雄异常) {
处理选择英雄异常;
} catch (载入游戏画面异常) {
处理载入游戏画面异常;
}
......
这里就是有异常处理的代码,当有一个地方不对劲,我们就会对其进行处理。总的来说,使用第一种方式,正常流程和错误处理流程代码混在一起,代码整体显的比较混乱。而第二种方式正常流程和错误流程是分离开的,更容易理解代码。
二.异常的基本语法
接下来我们学习一下异常的基本语法。

1.捕获异常
基本语法:
try{
有可能出现异常的语句 ;
}[catch (异常类型 异常对象) {
} ... ]
[finally {
异常的出口
}]
上面的启动王者的代码中,有异常处理的语法就是这套了,这里要说明几个点:
try
代码块中放的是可能出现异常的代码catch
代码块中放的是出现异常后的处理行为finally
代码块中的代码用于处理善后工作, 会在最后执行- 其中
catch
和finally
都可以根据情况选择加或者不加
对于try,catch和finally我们举个例子:
public static void main(String[] args) {
int [] array ={1,2,3};
System.out.println(array[5]);
//这里的数组就没有5下标的 很明显的数组越界
System.out.println("我不能打印出来了");
//并且我们在运行结果看见异常后 后面的代码不执行了
}
那么我们就可以把代码修改为这样,把这里我们对数组处理的时候可能的异常数组越界写上去:
public static void main(String[] args) {
int [] array ={1,2,3};
try{
System.out.println(array[5]);
System.out.println("我不能打印出来了");
//当异常抛出之后 这里在try里面的后面的代码都不会执行了
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("我捕捉到了一个数组越界异常了 这里就处理一下异常");
//数组越界异常打印
}
System.out.println("我在这可以打印");
//这样子后面的代码也不会被影响到 正常打印
}
所以这里的好处就是处理了异常,不会影响后面的代码执行,如果我们不处理异常,就会交给JVM进行处理,JVM进行处理程序就立马终止了!!
2.异常处理流程
那么对于捕获到异常的处理流程,我们从一个例子去讲起:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int arr = scanner.nextInt();
//一个简单的输入一个int类型的数字并打印
System.out.println(arr);
}
如果我们不是正常输入一个整数,我输入字符串,那正常来说就会报错了,所以我们来看一下这个错误分析:

然后对于异常处理,我们就按下面的步骤来:
而且在这里的异常信息栈中,我们应该先找蓝色字体的异常,因为灰色的是源码部分,源码部分的还能出错吗,所以这里是先修正蓝色异常再运行调试。
由这里例子引申出的其实只是冰山一角,我们在处理异常的时候更应该的是在不同场景下做出不同的考虑去决定:
对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果。
对于不太严重的问题(大多数场景),可以记录错误日志, 并通过监控报警程序及时通知程序猿。
对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试。
对于第二点,是我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置。
比如说:
public static void main(String[] args) {
int [] array ={1,2,3};
try{
array = null;
System.out.println(array[2]);
System.out.println("我不能打印出来了");
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("我捕捉到了一个数组越界异常!!");
}catch (NullPointerException e){
System.out.println("我捕捉到了一个空指针异常!!");
}
System.out.println("我在这可以打印");
}
这里虽然只有两个异常,但是如果再写一些,三个,四个,一百个的话,就很难找到了,所以我们可以在异常处理中加入一行代码,打印错误信息:
public static void main(String[] args) {
int [] array ={1,2,3};
try{
array = null;
System.out.println(array[2]);
System.out.println("我不能打印出来了");
}catch (ArrayIndexOutOfBoundsException e){
e.printStackTrace();
System.out.println("我捕捉到了一个数组越界异常!!");
}catch (NullPointerException e){
e.printStackTrace();
//打印错误信息
System.out.println("我捕捉到了一个空指针异常!!");
}
System.out.println("我在这可以打印");
}
运行结果:
当我们点击蓝色字体,它就会跳转到错误信息出现错误的地方,让我们更容易去找到错误,并对错误进行修改。
而对于异常处理来说,我们可以先跳转到下面的Java异常体系看一下,其实这些异常是有一个体系的,异常存在父子类关系,这时候我们就要考虑一下,对于父子类这一方面,在异常处理中会是什么样的,我们来看一下:
比如说上面的数组越界异常,我们在IDEA中按住ctrl点击异常信息,就会跳转到它的源码部分,我们可以看到它的父类是IndexOutOfBoundsException
的异常:
那我们将上面处理数组越界异常的代码写成这样,其实也是可以的,因为父类里面含有子类的异常:
public static void main(String[] args) {
int [] array ={1,2,3};
try{
System.out.println(array[5]);
System.out.println("111111");
}catch (IndexOutOfBoundsException e){
e.printStackTrace();
System.out.println("我捕捉到了一个数组越界异常!!");
}
System.out.println("222222");
}
那我们如果继续点一下IndexOutOfBoundsException
,再看它的父类,再点它的父类,最后会是什么呢,我们继续点,会出现下面的一个答案,到最后,其实 Exception
类是所有异常类的父类,因此可以用这个类型表示捕捉所有异常。
在这里值得注意的是:
-
catch 进行类型匹配的时候,不光会匹配相同类型的异常对象,也会捕捉目标异常类型的子类对象。如之前的代码,
NullPointerException
和ArrayIndexOutOfBoundsException
都是 Exception 的子类,因此都能被捕获到。 -
对于
catch
中既含有子类异常处理又含有父类异常处理,如果顺序是子类在前父类在后,就会正常运行;如果是父类在前就会报错,因为父类在前后面没必要有子类处理了。 -
虽然
Exception
类是所有异常类的父类,可以用于捕捉所有异常,但这个含量太大了,并且如果不注意,就不能明确的提示出是什么异常,不建议使用。
最后再讲一下我们的finally,finally
表示最后的善后工作, 例如释放资源。
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try{
int arr = scanner.nextInt();
System.out.println(arr);
}catch (InputMismatchException e){
e.printStackTrace();
System.out.println("输入有误!");
}catch (ArithmeticException e){
e.printStackTrace();
System.out.println("算数异常,可能是0作为除数");
}finally{
//一般用作 资源的关闭
scanner.close();
System.out.println("finally执行了");
}
}
注意:对于
finally
来说,不管是否结束,它都会执行,比如说我们遇到return
返回就是结束,但是还是会执行finally部分的,所以要尽量避免在finally中写return。
还有一些是如果本方法中没有合适的处理异常的方式, 就会沿着调用栈向上传递的代码,如果向上一直传递都没有合适的方法处理异常, 最终就会交给 JVM
处理, 程序就会异常终止(和我们最开始未使用 try catch 时是一样的):
public static void main(String[] args) {
try {
func();
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("after try catch");
}
public static void func() {
int[] arr = {1, 2, 3};
System.out.println(arr[100]);
//错了但是没有处理方法
}
所以结合上面所有内容,得到的一整套的异常处理流程就是:
- 程序先执行
try
中的代码 - 如果
try
中的代码出现异常, 就会结束try
中的代码, 看和catch
中的异常类型是否匹配 - 如果找到匹配的异常类型, 就会执行
catch
中的代码 - 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者
- 无论是否找到匹配的异常类型,
finally
中的代码都会被执行到(在该方法结束之前执行) - 如果上层调用者也没有处理的了异常, 就继续向上传递
- 一直到
main
方法也没有合适的代码处理异常, 就会交给JVM
来进行处理,此时程序就会异常终止
3.抛出异常及异常说明
除了 Java 内置的类会抛出一些异常之外,程序猿也可以手动抛出某个异常。使用 throw 关键字完成这个操作:
public class TestDemo {
public static int func(int x,int y) throws ArithmeticException{
if(y == 0){
throw new ArithmeticException("by zero!");
//抛出自定义异常
}
return x/y;
}
public static void main(String[] args) {
System.out.println(func(10, 0));
}
}
在这个代码中, 我们可以根据实际情况来抛出需要的异常,在构造异常对象同时可以指定一些描述性信息。
而对于上面的代码中,
throws
引用的是作为下面我们写的抛出自定义异常的声明,然后抛出自定义异常就是以throw
引导,两者通常成对出现。
三.Java的异常体系
对于Java中的异常体系,是这样的:

除了这里显示的,其实还有许许多多的子类等没有展现出来,但是对于异常来说太多了,我们只需要在需要用到的时候查看就可以啦,没必要全部记忆。回到这里,这最上面的顶层类 Throwable
派生出两个重要的子类——Error
和 Exception
。
其中 Error
指的是 Java 运行时内部错误和资源耗尽错误。 应用程序不抛出此类异常. 这种内部错误一旦出现,除了告知用户并使程序终止之外,再无能无力,这种情况很少出现,出现便必须由程序员自己去处理代码。
而Exception
是我们程序猿所使用的异常类的父类。其中 Exception 有一个子类称为 RuntimeException , 这里面又派生出很多我们常见的异常类NullPointerException , IndexOutOfBoundsException 等。
Java语言规范将派生于
Error
类或RuntimeException
类的所有异常称为 非受查异常, 所有的其他异常称为 受查异常。
所以我们的图就应该是这样子的:

如果是受查异常代码:
public static void main(String[] args) {
System.out.println(readFile());
}
public static String readFile() {
// 尝试打开文件, 并读其中的一行.
File file = new File("d:/test.txt");
// 使用文件对象构造 Scanner 对象.
Scanner sc = new Scanner(file);
return sc.nextLine();
}
在IDEA中显示是这样的:
明显我们看见Scanner是报红线的,然后提示中是未报告的异常错误java.io.FileNotFoundException
; 必须对其进行捕获或声明以便抛出。如 FileNotFoundException
这样的异常就是受查异常. 如果不显式处理, 编译无法通过。
当然我们可以点击Scanner然后 alt + enter
让IDEA适配一个解决办法,
比如我们选择第一个增加异常处理的方法:

然后我们就增加了异常声明,就可以通过编译:
同样的也可以用上面说到的try catch去包裹:
public static void main(String[] args) {
System.out.println(readFile());
}
public static String readFile() {
File file = new File("d:/test.txt");
Scanner sc = null;
try {
sc = new Scanner(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return sc.nextLine();
}
四.自定义异常类
Java 中虽然已经内置了丰富的异常类, 但是我们实际场景中可能还有一些情况需要我们对异常类进行扩展, 创建符合我们实际情况的异常。
比如说我们要实现一个用户登录的过程:
public class Test {
private static String userName = "admin";
private static String password = "123456";
public static void main(String[] args) {
login("admin", "123456");
}
public static void login(String userName, String password) {
if (!Test.userName.equals(userName)) {
//处理用户名错误
}
if (!Test.password.equals(password)) {
//处理密码错误
}
System.out.println("登陆成功");
}
}
此时我们在处理用户名密码错误的时候可能就需要抛出两种异常。我们可以基于已有的异常类进行扩展(继承),创建和我们业务相关的异常类。也就是处理用户名错误和处理密码错误。
class UserError {
//处理用户名错误
}
class PasswordError {
//处理密码错误
}
但是实际上这样子写还是会出错的,因为如果我们要自定义自己的异常,我们需要将其继承在Java中本身就含有的异常中,不然是无法识别的,所以我们应该写为:
class UserError extends Exception {
//继承于异常
public UserError(String message) {
super(message);
}
}
class PasswordError extends Exception {
//继承于异常
public PasswordError(String message) {
super(message);//
}
}
然后源代码改为:
class UserError extends Exception {
public UserError(String message) {
super(message);//提供构造方法
}
}
class PasswordError extends Exception {
public PasswordError(String message) {
super(message);//提供构造方法
}
}
public class TestDemo2 {
private static String userName = "admin";//默认用户名
private static String password = "123456";//默认密码
public static void main(String[] args) {
try {
login("admin", "123456");//调用方法
} catch (UserError userError) {
userError.printStackTrace();//用户名异常处理
} catch (PasswordError passwordError) {
passwordError.printStackTrace();//密码异常处理
}
}
public static void login(String userName, String password) throws UserError,
PasswordError {
if (!TestDemo2.userName.equals(userName)) {
throw new UserError("用户名错误");//用户名不相等,捕获用户名错误异常
}
if (!TestDemo2.password.equals(password)) {
throw new PasswordError("密码错误");//密码不相等,捕获密码错误异常
}
System.out.println("登陆成功");//都正常则登录成功
}
}
但是继承也会有讲究,还记得我们上面Java结构体系的图吗,没错就是下面这张,如果我们继承的是Exception
的话,我们就相当于是继承了受查异常,也就是要把处理异常用try catch包裹或者用throws和throw引用,如果是继承Runtime Exception
的就是非受查异常。

最后是注意事项:
- 自定义异常通常会继承自
Exception
或者RuntimeException
- 继承自
Exception
的异常默认是受查异常 - 继承自
RuntimeException
的异常默认是非受查异常
这就是本篇异常的全部内容啦对于Java从零到一系列也算是完结了,接下来会写一些有关于数据结构的博客。欢迎关注。一起学习,共同努力!也可以期待下个系列接下来的博客噢。
还有一件事: