# Java中的异常控制
## 一、前言概述
写程序时发生错误在所难免,对错误的控制一直是编程人员要解决的一大问题,Java中的异常控制就为了帮助我们规避在语言层面可能发生的错误(业务逻辑层面的错误还是我们程序员自己的锅)。 java中的异常控制是java语言的强大之处的体现之一。
在很多编程语言中经常将“异常控制”模块内置到程序设计语言本身,有时甚至内建到操作系统内,但是有时并不会强制用户(程序员)使用,即不会在一些可能发生异常的情况下强制我们处理它。但是java与大多数编程语言不同的是它有时候将**强制我们处理**它们(异常),处理的方式是`抛出`和`捕获`。而且就算我们不处理发生的异常,最后JVM也会帮我们处理(即中断程序的运行,并在控制台上进行显示)。
> 由于很难设计出一套完美的错误控制方案,许多语言干脆将问题简单地忽略掉,将其转嫁给库设计人员。对大多数错误控制方案来说,最主要的一个问题是它们严重依赖程序员的警觉性,而不是依赖语言本身的强制标准。《java编程思想》
## 二、 Java的异常体系
Java中“一切皆对象”,同样的java中的异常(Exception)体系也是由各种各样的类构成的。Java中异常的根类(除Object外最上面一层)是`java.lang.Throwable`,在其下面有两个直接的子类:java.lang.`Error`与java.lang.`Exception`,而我们平常所说的异常指的就是Exception及其子类。
至于发生Error错误一般是属于系统内部出现的问题,例如堆栈溢出(StackOverFlowError)、内存溢出(OutMemoryError)等,这些一般和JVM有关,有时候可能需要我们调整一些参数解决,但是错误的发生仍可能存在。
最后要注意的是异常类并不是只在java.lang包中,还可能出现在你自定义的包中,引人的第三方包中。
Java异常体系如下:
![java异常继承体系](https://img-blog.csdnimg.cn/20200831124536263.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE4NDk5MA==,size_16,color_FFFFFF,t_70#pic_center)
### 2.1 异常分类
Java中一般会将异常分为`免检异常`和`必检异常`。
#### 免检异常:RuntimeException
指的是在编程过程中允许我们不用手动处理,可以交给JVM处理,而JVM的处理方式就是中断程序运行。这类异常一般都是**运行时期的异常**(RuntimeException),即在程序运行时才可能出现,而且一般这种情况是我们程序员自己造成的,比如说数组越界异常、空指针的引用异常等等。
#### 必检异常:Exception
在编程过程中必须要手动处理的,处理的方式可以是“抛出”或者是“捕获”。如果是做“抛出”处理的话,最后一层可以抛出给JVM去处理(仍然是中断处理),“捕获”的话就是我们自己来手动处理。这类异常也称为**编译异常**。即在编译时必须处理,如果不处理,编译不能通过,就跟我们的语法错误一样,编译器会给我们报错。
对于需要处理的异常,在IDEA中会给我们两种选择处理的方式(对于这两种方式更进一步的还会在下面讲到):
![IDEA中处理异常的方式](https://img-blog.csdnimg.cn/20200831124439748.png#pic_center)
由上图的【Java异常体系】可知,异常体系的基类是Exception,而Exception又是继承Throwable,其实查看各种异常类的源码可以发现,各种异常类中基本就只有构造方法和一个序列号ID,而构造方法里面又只调用父类的构造方法,这样沿着继承链反向查看回去,会发现**最终调用的还是Throwable中的构造方法。**
例如:空指针异常类中的内容
![NullPointerException类结构](https://img-blog.csdnimg.cn/20200831124636833.png#pic_center)
里面的构造方法:(IDEA里面鼠标移上按住Crtl+单击就能进去看了)
~~~
//NullPointerException类
public NullPointerException() {
super();
}
public NullPointerException(String s) {
super(s);
}
//最终的Throwable类的构造方法
public Throwable() {
fillInStackTrace(); //此方法记录此Throwable对象信息,了解当前线程的堆栈帧的当前状态。
}
public Throwable(String message) {
fillInStackTrace();
detailMessage = message;
}
~~~
#### Throwable中的常用方法
* public void printStackTrace():打印异常的详细信息,包括异常的发生路径,异常的原因,及异常的种类;
* public String getMessage():获取异常发生的原因;
* public String toString():获取异常的类型和异常的描述信息,现在已经不用了。
### 2.2 异常产生过程举例
#### 举例
下面举一个数组越界异常(ArrayIndexOfBoundsException)来看看异常产生的大概过程。
1. 写一个方法来获取数组中的元素,传递的参数为一个数组和要获取元素的索引
~~~
public static int getElement(int[] nums, int index) {
return nums[index];
}
~~~
2. 接着在测试中传入数组和要获取元素的索引
~~~
public static void main(String[] args){
int[] nums = {1, 2, 3, 4, 5};
System.out.println(getElement(nums, 5));
}
~~~
3. 因为索引**5**超过了数组的长度,获取元素失败,这个时候就会抛出ArrayIndexOfBoundsException
~~~
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at com.smrobot.exception.demo1.getElement(demo1.java:47)
at com.smrobot.exception.demo1.main(demo1.java:13)
~~~
由上面可以看出`ArrayIndexOfBoundsException`是一个在代码运行期间才可能会发生的异常(运行期异常),而让这个异常产生的原因往往是因为我们自己的原因,例如这个例子中传入了越界的下标。
#### 异常产生的过程
1. 代码在main中执行,执行了
~~~
System.out.println(getElement(nums, 5));
~~~
2. 调用getElement方法,并且传入越界的下标,由于nums数组的最大长度才为5,找不到为5的索引,因此导致运行时发生的异常。而这个异常JVM认识,因此会将这个异常从该方法中抛出给调用者,即main()方法。
~~~
public static int getElement(int[] nums, int index) {
return nums[index]; // JVM 会在这里做这么一个操作:throw new ArrayIndexOfBoundsException(5);
}
~~~
3. main()方法接收到了JVM在getElement方法中抛出的异常对象。由于main()方法也没有处理该异常的方法,因此为继续将异常抛出给main()的调用者JVM。而JVM处理该异常的方式就是将异常对象的名称,内容,异常发生的调用栈打印出来,并且终止程序。
![ArrayIndexOfboundsException异常举例](https://img-blog.csdnimg.cn/20200831124725159.png#pic_center)
## 三、异常处理
### 相关关键字
* try:和catch一起使用,来用包裹可能出现异常的代码块;
* catch:和try一起使用,捕获try中可能出现的异常;
* finally:和try...catch一起使用,不管有没有异常发生中断程序的运行,finally代码块都会运行;
* throw:用来在方法中抛出一个异常;
* throws:用来声明方法抛出的异常。
### throw抛出异常
当我们写一个方法时,为了确保调用者传递的参数符合我们的想要的规范,以致于能让程序健壮运行,这个时候我们就可以使用抛出异常的方式来对传递的参数进行提前判断,如果不符合要求,即可抛出一个异常让调用者知道。而java中抛出异常的关键字就是throw。
使用格式如下:
> throw new 异常类(参数);
举例:`Objects`类中的非空判断
~~~
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
~~~
类似的java的源码中经常可以看到,提前对传递的参数进行判断,增强代码的健壮性。
当程序执行到了throw语句时,就会停止继续执行下去,而将对应的异常“返回”给该方法的调用者。(可以将之看成是类似return的效果,即"return"一个异常对象)
那么抛出的异常可以怎么样处理呢?一种是用throws声明抛出的异常,让调用者处理;一种是自己用try...catch处理,而不进行抛出了。
### throws声明异常
由于抛出的异常一般是将该异常从一个方法中抛出去(main()方法也是一样的),所以声明抛出的异常也是写在方法的定义中,就跟声明函数的返回值一样。
使用格式如下:
> 修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
举例:
~~~
public class ThrowsDemo2 {
public static void main(String[] args) throws IOException {
read("a.txt");
}
public static void read(String path) throws FileNotFoundException, IOException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 如果文件名不是a.txt就认为文件不存在
throw new FileNotFoundException("文件不存在");
}
if (!path.equals("b.txt")) {
//文件不为b.txt就抛出IOException,只是举例用
throw new IOException();
}
}
}
~~~
对于方法中抛出的必检异常(编译异常),如果方法内部本身没有处理,则必须使用throws声明,提示调用者去处理,不处理在IDEA中就会**标红**,并且不能编译通过。而对于运行时期的异常,不进行声明也是可以的,最后JVM会帮助我们进行中断处理。
### try...catch捕获异常
前面的throw和throws并不会真正让我们自己处理异常发生时要进行的操作,如果我们不处理,最后就是交给JVM中断程序运行的方式来处理了。try...catch就是可以让我们捕获处理异常,不至于让程序中断。
使用语法:
~~~
try {
//可能出现异常的代码
} catch(异常类型1 e) {
//处理异常的代码
//常用的有记录日志,打印异常信息,继续抛出异常给调用者处理
//详细方法的调用可以看上面的:Throwable中常用的方法
}
~~~
**捕获多个异常时的注意事项**
1. 如果在try代码块中可能存在多个异常需要捕获,那么可以使用多个catch进行捕获,大致的使用方式如下:
~~~
try {
} catch(异常类型1 e) {
} catch(异常类型2 e) {
}...
~~~
注意,在这种处理方式中,如果存在子父类异常,那么需要将子类异常声明在上面,父类异常声明在下面。例如,`ArrayIndexOutOfBoundsException`就是`IndexOutOfBoundsException`的子类(具体可查看API或源码),因此使用上面的方式分别捕获这两种异常的时候需要类似于下面的书写方式
~~~
try {
} catch(ArrayIndexOutOfBoundsException e) {
//子类异常放在上面
} catch(IndexOutOfBoundsException e) {
//父类异常放在下面
}
~~~
2. 也可以直接使用一个最大的异常对象Exception进行捕获,这样就不用写太多的catch代码块了
~~~
try {
} (Exception e) {
//使用异常的基类来捕获,这样所有可能发生的异常都会被捕获到,当然捕获到是最新发生的异常
}
~~~
### finally代码块
finally代码块是和try...catch一起使用,并在放到最后面来使用,常用来关闭各种资源,例如关闭IO资源、数据库连接资源、锁资源等等。
finally可以保证不管程序是否出现异常了,相关的资源都可以被关闭掉,因为finally代码块是一定会执行的(不转牛角尖的话!)。
使用语法如下:
~~~
try {
//编写可能出现异常的代码
} catch (Exception e) {
//处理异常
} finally {
//关闭已打开的资源
}
~~~
注意:只要当在try或者catch代码块中调用了退出JVM的方法(System.exit(0))时,finally代码块才不会执行,不然都会执行的;同时,即使在try或者catch中return语句,finally中的代码也是会被执行的;如果finally有return语句,那么程序最终返回的是finally代码块中return语句的内容。
## 四、自定义异常
我们不仅可以用Java中已经帮我们定义好的异常,也可以自定义属于自己的异常,来满足自己的业务需求,例如考试成绩是负数的异常、年龄是负数的异常...
自定义异常的方式如下:
1. 如果是自定义编译期异常,自定义一个类并且继承Exception;
2. 如果是自定义运行期异常,自定义一个类并且继承RuntimeException;
举例如下:
~~~
// 自己自定义的业务异常 -- 登录异常
public class LoginException extends Exception {
//给个序列化ID
private static final long serialVersionUID = -5116101128118950844L;
/**
* 空参构造,调用父类空参构造
*/
public LoginException() {
super();
}
/**
* 带参构造,可传入自定义的提示信息
*/
public LoginException(String message) {
super(message);
}
} //可以仿照JDK中各种异常类的定义方法
~~~
自定义的异常的使用方法和使用JDK中的异常方式是一样的,只不过自定义的异常更加符合自己想要达到的效果罢辽~
## 五、写在最后
![死了都要try](https://img-blog.csdnimg.cn/20200831124819136.bmp?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE4NDk5MA==,size_16,color_FFFFFF,t_70#pic_center)
【参考】《Java编程思想》
- 第一章 Java基础
- ThreadLocal
- Java异常体系
- Java集合框架
- List接口及其实现类
- Queue接口及其实现类
- Set接口及其实现类
- Map接口及其实现类
- JDK1.8新特性
- Lambda表达式
- 常用函数式接口
- stream流
- 面试
- 第二章 Java虚拟机
- 第一节、运行时数据区
- 第二节、垃圾回收
- 第三节、类加载机制
- 第四节、类文件与字节码指令
- 第五节、语法糖
- 第六节、运行期优化
- 面试常见问题
- 第三章 并发编程
- 第一节、Java中的线程
- 第二节、Java中的锁
- 第三节、线程池
- 第四节、并发工具类
- AQS
- 第四章 网络编程
- WebSocket协议
- Netty
- Netty入门
- Netty-自定义协议
- 面试题
- IO
- 网络IO模型
- 第五章 操作系统
- IO
- 文件系统的相关概念
- Java几种文件读写方式性能对比
- Socket
- 内存管理
- 进程、线程、协程
- IO模型的演化过程
- 第六章 计算机网络
- 第七章 消息队列
- RabbitMQ
- 第八章 开发框架
- Spring
- Spring事务
- Spring MVC
- Spring Boot
- Mybatis
- Mybatis-Plus
- Shiro
- 第九章 数据库
- Mysql
- Mysql中的索引
- Mysql中的锁
- 面试常见问题
- Mysql中的日志
- InnoDB存储引擎
- 事务
- Redis
- redis的数据类型
- redis数据结构
- Redis主从复制
- 哨兵模式
- 面试题
- Spring Boot整合Lettuce+Redisson实现布隆过滤器
- 集群
- Redis网络IO模型
- 第十章 设计模式
- 设计模式-七大原则
- 设计模式-单例模式
- 设计模式-备忘录模式
- 设计模式-原型模式
- 设计模式-责任链模式
- 设计模式-过滤模式
- 设计模式-观察者模式
- 设计模式-工厂方法模式
- 设计模式-抽象工厂模式
- 设计模式-代理模式
- 第十一章 后端开发常用工具、库
- Docker
- Docker安装Mysql
- 第十二章 中间件
- ZooKeeper