1. 异常概述
1.1. 异常的概念
Java中的异常是指Java程序在运行时可能出现的错误或非正常情况,比如在程序中试图打开一个根本不存在的文件,在程序中除0等。异常是否出现,通常取决于程序的输入、程序中对象的当前状态以及程序所处的运行环境。程序抛出异常之后,会对异常进行处理。异常处理将会改变程序的控制流程,出于安全性考虑,同时避免异常程序影响到其他正常程序的运行,操作系统通常将出现异常的程序强行中止,并弹出系统错误提示。
下面通过一个案例认识一下什么是异常,在本案例中,计算以0为除数的表达式,运行程序并观察程序的运行结果。
public class TestDemo {
@Test
public void test() {
int result = divide(4, 0); // 调用divide()方法,第2个参数为0
System.out.println(result);
}
/**
* 下面的方法实现了两个整数相除
* @param x
* @param y
* @return
*/
public int divide(int x, int y) {
int result = x / y; //定义一个变量result记录两个数相除的结果
return result; //将结果返回
}
}
运行代码,控制台显示的运行结果如下图所示。
由上图可知,程序发生了算术异常(ArithmeticException),提示运算时出现了被0除的情况。异常发生后,程序会立即结束,无法继续向下执行。
1.2. 异常类
Java提供了大量的异常类,每一个异常类都表示一种预定义的异常,这些异常类都继承自java.lang包下的Throwable类。Throwable类的继承体系图如下所示。
由上图中可知,Throwable类是所有异常类的父类,它有两个直接子类Error类和Exception类,其中,Error类代表程序中产生的错误,Exception类代表程序中产生的异常。
(1)Error类
Error类称为错误类,它表示Java程序运行时产生的系统内部错误或资源耗尽的错误,这类错误比较严重,仅靠修改程序本身是不能恢复执行的。例如,使用java命令去运行一个不存在的类就会出现Error错误。
(2)Exception类
Exception类称为异常类,它表示程序本身可以处理的错误,在Java程序中进行的异常处理,都是针对Exception类及其子类的。在Exception类的众多子类中有一个特殊的子类—RuntimeException类,RuntimeException类及其子类用于表示运行时异常。 Exception类的其他子类都用于表示编译时异常。
(3)Throwable类的常用方法
Throwable类中的常用方法,具体如下。
方法声明 | 功能描述 |
String getMessage() | 返回异常的消息字符串 |
String toString() | 返回异常的简单信息描述 |
void printStackTrace() | 获取异常类名和异常信息,以及异常出现在程序中的位置,把信息输出在控制台。 |
2. 运行时异常与编译时异常
2.1. 编译时异常
在实际开发中,经常会在程序编译时产生异常,这些异常必须要进行处理,否则程序无法正常运行,这种异常被称为编译时异常,也称为checked异常。在Exception类中,除了RuntimeException类及其子类,Exception的其他子类都是编译时异常。编译时异常的特点是Java编译器会对异常进行检查,如果出现异常就必须对异常进行处理,否则程序无法通过编译。
处理编译时期的异常有两种方式,具体如下:
-
使用try…catch语句对异常进行捕获处理。
-
使用throws关键字声明抛出异常,调用者对异常进行处理。
2.2. 运行时异常
另一种异常是在程序运行时产生的,这种异常即使不编写异常处理代码,依然可以通过编译,因此被称为运行时异常,也称为unchecked异常。RuntimeException类及其子类都是运行时异常。运行时异常的特点是在程序运行时由Java虚拟机自动进行捕获处理的,Java编译器不会对异常进行检查。也就是说,当程序中出现这类异常时,即使没有使用try…catch语句捕获或使用throws关键字声明抛出,程序也能编译通过,只是程序在运行过程中可能报错。
在Java中,常见的运行时异常有多种,具体如下所示。
方法声明 | 功能描述 |
ArithmeticException | 算术异常 |
IndexOutOfBoundsException | 索引越界异常 |
ClassCastException | 类型转换异常 |
NullPointerException | 空指针异常 |
NumberFormatException | 数字格式化异常 |
运行时异常一般是由程序中的逻辑错误引起的,在程序运行时无法恢复。例如,通过数组的索引访问数组的元素时,如果索引超过了数组范围,就会发生索引越界异常,代码如下所示:
int[] arr=new int[5];
System.out.println(arr[6]);
在上面的代码中,由于数组arr的length为5,最大索引应为4,当使用arr[6]访问数组中的元素就会发生数组索引越界的异常。
2.3. 运行时异常案例
(1)算术异常
ArithmeticException,算术异常,在做整数的除法或整数求余运算时可能产生的异常,它是在除数为零时产生的异常。
public class TestDemo {
@Test
public void test() {
int num = 1 / 0;
}
}
(2)数组下标越界异常
ArrayIndexOutOfBoundsException,数组下标越界异常,当引用数组元素的下标超出范围时产生的异常。
public class TestDemo {
@Test
public void test() {
int []num = new int[5];
System.out.println(num[5]);
}
}
(3)类型转换异常
public class TestDemo {
@Test
public void test() {
ArrayList list = new ArrayList();
list.add("1111");
Integer num = (Integer) list.get(0);
}
}
(4)空指针异常
NullPointerException,空指针异常,即当某个对象的引用为null时调用该对象的方法或使用对象会产生该异类。
public class TestDemo {
@Test
public void test() {
String name = null;
System.out.println(name.length());
}
}
(5)数字格式错误异常
NumberFormatException,数字格式错误异常,在将字符串转换为数值时,如果字符串不能正确转换成数值则产生该异常。
public class TestDemo {
@Test
public void test() {
Double d = Double.parseDouble("1.2m1");
}
}
3. 异常处理
在Java中,通过try、catch、finally、throw、throws这5个关键字进行异常对象的处理。具体说明如下所示。
关键字 | 功能描述 |
try | 里面放置可能引发异常的代码 |
catch | 后面对应异常类型和一个代码块,该关键字表明catch块是用于处理这种类型的代码块 |
finally | 主要用于回收在try代码块里打开的物理资源,如数据库连接、网络连接和磁盘文件。异常机制保证finally块总是被执行 |
throw | 用于抛出一个实际的异常。它可以单独作为语句来抛出一个具体的异常对象 |
throws | 用在方法签名中,用于声明该方法可能抛出的异常 |
3.1. try...catch语句
为了使发生异常后的程序代码正常执行,程序需要捕获异常并进行处理,Java提供了try…catch语句用于捕获并处理异常。try…catch语句的语法格式如下所示:
try{
代码块
}catch(ExceptionType e){
代码块
}
(1)try...catch语句编写注意事项
-
try代码块是必需的。
-
catch代码块和finally代码块都是可选的,但catch代码块和finally代码块至少要出现一个。
-
catch代码块可以有多个,但捕获父类异常的catch代码块必须位于捕获子类异常的catch代码块后面。
-
catch代码块必须位于try代码块之后。
(2)try...catch语句异常处理流程
由上图可知,程序通过try语句捕获可能出现的异常,如果try语句没有捕获到异常,则直接跳出try…catch语句块执行其他程序;如果在try语句中捕获到了异常,则程序会自动跳转到catch语句中找到匹配的异常类型进行相应的处理。异常处理完毕,最后执行其他程序语句。
(3)try...catch语句异常处理案例
public class TestDemo {
@Test
public void test() {
try {
int result = divide(4, 0); //调用divide()方法
System.out.println(result);
} catch (Exception e) { //对异常进行处理
System.out.println("捕获的异常信息为:" + e.getMessage());
}
System.out.println("程序继续向下执行...");
}
/**
* 下面的方法实现了两个整数相除
* @param x
* @param y
* @return
*/
public int divide(int x, int y) {
int result = x / y; //定义一个变量result记录两个数相除的结果
return result; //将结果返回
}
}
3.2. finally语句
在程序中,有时候会希望一些语句无论程序是否发生异常都要执行,这时就可以在try…catch语句后,加一个finally代码块。finally语句的语法格式如下所示:
try{
代码块
} catch(ExceptionType e){
代码块
} finally{
代码块
}
注意:finally代码块必须位于所有catch代码块之后。
(1)try…catch…finally语句的异常处理流程
由上图可知,在try…catch…finally语句中,不管程序是否发生异常,finally代码块中的代码都会被执行。需要注意的是,如果程序发生异常但是没有被捕获到,在执行完finally代码块中的代码之后,程序会中断执行。
(2)try…catch…finally语句的异常处理案例
public class TestDemo {
@Test
public void test() {
//下面的代码定义了一个try…catch…finally语句用于捕获异常
try {
int result = divide(4, 0); //调用divide()方法
System.out.println(result);
} catch (Exception e) { //对捕获到的异常进行处理
System.out.println("捕获的异常信息为:" + e.getMessage());
return; //用于结束当前语句
} finally {
System.out.println("进入finally代码块");
}
System.out.println("程序继续向下…");
}
/**
* 下面的方法实现了两个整数相除
*
* @param x
* @param y
* @return
*/
public int divide(int x, int y) {
int result = x / y; //定义一个变量result记录两个数相除的结果
return result; //将结果返回
}
}
注意:如果在try...catch中执行了System.exit(0)语句,finally代码块不再执行。System.exit(0)表示退出当前的Java虚拟机,Java虚拟机停止了,任何代码都不能再执行了。
4. 抛出异常
方法运行过程中如果产生了异常,在这个方法中就生成一个代表该异常类的对象,并把它交给运行时系统,运行时系统寻找相应的代码来处理该异常。这个过程称为抛出异常。
4.1. throws关键字
有时方法中产生的异常不需要在该方法中处理,可能需要由该方法的调用方法处理,这时可以在声明方法时用throws子句声明抛出异常,将异常传递给调用该方法的方法处理,调用者在调用方法时,就明确地知道该方法有异常,并且必须在程序中对异常进行处理,否则编译无法通过。
使用throws关键字抛出异常的语法格式如下所示:
修饰符 返回值类型 方法名(参数) throws 异常类1,异常类2...{
方法体
}
(1)使用throws关键字抛出异常案例
可以使用try…catch语句处理divide()方法抛出异常,也可以继续使用throws关键字将Exception抛出。
public class TestDemo {
@Test
public void test(){
//下面的代码定义了一个try…catch语句用于捕获异常
try {
int result = divide(4, 2); //调用divide()方法
System.out.println(result);
} catch (Exception e) { //对捕获到的异常进行处理
e.printStackTrace(); //打印捕获的异常信息
}
}
/**
* 下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常
* @param x
* @param y
* @return
* @throws Exception
*/
public int divide(int x, int y) throws Exception {
int result = x / y; //定义一个变量result记录两个数相除的结果
return result; //将结果返回
}
}
4.2. throw关键字
在Java程序中,除了throws关键字,还可以使用throw关键字抛出异常。与throws关键字不同的是,throw关键字用于方法体内,抛出的是一个异常实例,并且每次只能抛出一个异常实例。
使用throw关键字抛出异常的语法格式如下所示:
throw ExceptionInstance;
(1)使用throw关键字抛出异常案例
public class TestDemo {
@Test
public void test(){
// 下面的代码定义了一个try…catch语句用于捕获异常
int age = -1;
try {
printAge(age);
} catch (Exception e) { // 对捕获到的异常进行处理
System.out.println("捕获的异常信息为:" + e.getMessage());
}
}
/**
* 定义printAge()输出年龄
* @param age
* @throws Exception
*/
public void printAge(int age) throws Exception {
if(age <= 0){
// 对业务逻辑进行判断,当输入年龄为负数时抛出异常
throw new Exception("输入的年龄有误,必须是正整数!");
}else {
System.out.println("此人年龄为:"+age);
}
}
}
5. 异常处理综合案例
网页前端输入学生学号、年龄、姓名信息,进行保存。输入信息需满足,学号长度为4位、年龄大于0才可以保存成功,否则提示保存失败及失败原因。
-
Student类
public class Student {
private String no;
private String name;
private int age;
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"no='" + no + '\'' +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
-
StudentController
public class StudentController {
/**
* 接收前端输入信息
* @param student
*/
public void addStudent(Student student) throws Exception{
StudentService service = new StudentService();
service.saveStudent(student);
}
}
-
StudentService
public class StudentService {
/**
* 保存学生信息
* @param student
* @throws Exception
*/
public void saveStudent(Student student) throws Exception {
//校验学号是否正确,学号为4位
if(student.getNo() == null || student.getNo().length() != 4){
throw new Exception("学号信息错误:" + student.getNo());
}
//校验学生年龄
if(student.getAge() <= 0){
throw new Exception("年龄信息错误:" + student.getAge());
}
System.out.println("保存学生信息成功,保存的学生信息是:" + student);
}
}
-
测试类
public class TestDemo {
/**
* 模拟前端输入学生信息保存
*/
@Test
public void test(){
Student student = new Student();
student.setNo("1001");
student.setName("张三");
student.setAge(20);
StudentController controller = new StudentController();
try{
controller.addStudent(student);
}catch (Exception e){
System.out.println("保存学生信息失败,失败原因是:" + e.getMessage());
}
}
}
6. Debug调试
Debug用来追踪代码的运行流程,通常在程序运行过程中出现异常,启用Debug模式可以分析定位异常发生的位置,以及在运行过程中参数的变化。
6.1. 设置断点
在左边行号栏单击左键,或者快捷键Ctrl+F8打上/取消断点,断点行的颜色可自己去设置。
6.2. 启动Debug模式
以Debug模式启动服务,左边的一个按钮则是以Run模式启动。在开发中,我一般会直接启动Debug模式,方便随时调试代码。
6.3. 调试按钮
Step Over (F8):步过,一行一行地往下走,如果这一行上有方法不会进入方法。
Step Into (F7):步入,如果当前行有方法,可以进入方法内部,一般用于进入自定义方法内,不会进入官方类库的方法,如第25行的put方法。
Step Out (Shift + F8):步出,从步入的方法内退出到方法调用处,此时方法已执行完毕,只是还没有完成赋值。
Resume Program (F9):恢复程序,比如,你在第20行和25行有两个断点,当前运行至第20行,按F9,则运行到下一个断点(即第25行),再按F9,则运行完整个流程,因为后面已经没有断点了。