目录
前言
一、异常的概念与体系结构
(一)异常的概念
(二)异常的体系结构
(三)异常的分类
1.编译时异常(区别于语法错误)
3.运行时异常
二、异常的处理
(一)防御式异常
1.LBYL:Look Before You Leap.在操作之前就做充分的检查,即:事前防御型
2.EAFP:先操作,遇到问题再处理:即事后认错型
(二)异常的抛出
(三)异常的捕获
1.try-catch捕获并处理(重要)
2.异常声明throws
3.finally
(四)程序处理流程
三、自定义异常类
总结
前言
当我们写下代码运行程序出错时,或者当项目较大需要找出 bug 时,或者当我们需要实现一些对自己代码的问题提示时,都避不开异常,所以我们有必要对异常进行专门的学习。作者建议,在看完第二部分的(一)之后,先看第二部分的(三)的第一点,再看异常的抛出和之后的内容
一、异常的概念与体系结构
(一)异常的概念
在 Java 中,将程序执行过程中发生的不正常行为称为异常。比如:
- 算术异常
System.out.println(10 / 0);
//执行过程
Exception in thread "main" java.lang.ArithmeticException: / by zero
- 数组越界异常
int[] arr = new int[]{1,2,3};
System.out.println(arr[4]);
//执行过程
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
- 空指针异常
String s = null;
System.out.println(s.indexOf(2));
//执行过程
Exception in thread "main" java.lang.NullPointerException
从上述过程中可以看出,Java 中不同类型的异常,都有与其对应的类来进行描述
(二)异常的体系结构
异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java 内部维护了一个异常的体系结构:
从上图中可以看到:
- Throwable:是异常体系的顶层类,其派生出两个重要的子类,Error 和 Exception
- Error:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError,一旦发生回力乏术
- Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Exception
(三)异常的分类
异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为:
1.编译时异常(区别于语法错误)
在程序编译期间发生的异常,称为编译时异常,也称为受检查异常(Checked Exception)。(程序要想运行,必须处理这个异常)
class Person {
private String name;
private int gender;
private int age;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
//编译时异常
public static void main(String[] args) {
Person person = new Person();
Person person1 = (Person)person.clone();
}
}
//编译时异常
//未报告的异常错误java.lang.CloneNotSupportedException; 必须对其进行捕获或声明以便抛出
在未运行程序时报出的异常:
3.运行时异常
在在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常(Unchecked Exception)
RunTimeException以及其子类对应的异常,都称为运行时异常。比如:NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException
注意:编译时出现的语法性错误,不能称之为异常。例如将 System.out.println 拼写错了, 写成了system.out.println,此时编译过程中就会出错, 这是 "编译期" 出错。而运行时指的是程序已经编译通过得到class 文件了, 再由 JVM 执行过程中出现的错误
二、异常的处理
(一)防御式异常
1.LBYL:Look Before You Leap.在操作之前就做充分的检查,即:事前防御型
缺陷:执行一步检测一步,正常流程和错误处理流程代码混在一起,代码整体显得混乱
2.EAFP:先操作,遇到问题再处理:即事后认错型
try {
...
} catch(){
//通过 catch 捕获可能出现的异常
...
} catch(){
...
}...
//后续代码继续执行
优势:正常流程和错误流程是分开的,程序员更关注正常流程,代码更清晰,容易理解代码
异常处理的核心思想就是EAFP
在 Java 中,异常处理主要的五个关键字:throw、try、catch、final、throws
(二)异常的抛出
在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调用者,比如:参数检测
在Java中,可以借助 throw 关键字,抛出一个指定的异常对象,将错误信息告知给调用者。(程序员主动抛出)具体语法如下:
throw new XXXException("异常产生的原因");
代码示例:
//利用 throw 主动抛出异常
public static void test1(int x) {
if(x == 1) {
throw new RuntimeException();
}
}
public static void main(String[] args) {
test1(1);
}
运行结果(这是没有接收异常的情况):
当我们主动接收异常时,代码示例:
//方法一
public static void test1(int x) {
if(x == 1) {
throw new RuntimeException();
}
}
public static void main(String[] args) {
try {
test1(1);
} catch (RuntimeException e){
e.printStackTrace();
}
System.out.println("程序继续执行");
}
//方法二
public static void test1(int x) {
try {
if(x == 1) {
throw new RuntimeException();
}
} catch (RuntimeException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
test1(1);
System.out.println("程序继续执行");
}
运行结果:
注意事项:
- throw 必须写在方法体内部
- 抛出的对象必须是 Exception 或者 Exception 的子类对象
- 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给 JVM 来处理(但后果就是程序异常终止)
- 如果抛出的是编译时异常,用户必须处理,否则通不过编译
- 异常一旦抛出,其后的代码就不会执行了(如果用try-catch接收了,则只是 try 内后续的代码不执行)
(三)异常的捕获
异常的捕获,也就是异常的具体处理方式,主要有两种:异常声明 throws 以及 try-catch 捕获处理
1.try-catch捕获并处理(重要)
举例:
public static void main(String[] args) {
try {
System.out.println(10 / 0);
}catch(ArithmeticException e) {
//快速地定位异常出现的位置
e.printStackTrace();
System.out.println("算术异常!");
}catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("数组越界异常!");
}catch (NullPointerException e) {
e.printStackTrace();
System.out.println("空指针异常!");
}
System.out.println("程序继续执行!");
}
运行结果:
注意事项:
- try块内抛出异常位置之后的代码将不会被执行
- 如果抛出异常类型与 catch 时异常类型不匹配,即异常不会被成功捕获,也就是不会被处理,继续往外抛,直到 JVM 收到后中断程序——异常是按照类型来捕获的
- try中可能会抛出多个不同类型的异常对象,则必须用多个 catch 来捕获——即多种异常,多次捕获
- 如果多个异常的处理方式是完全相同,也可以写成这样
代码示例:
public static void main(String[] args) {
try {
System.out.println(10 / 0);
}catch(ArithmeticException | ArrayIndexOutOfBoundsException | NullPointerException e) {
//快速地定位异常出现的位置
e.printStackTrace();
}
System.out.println("程序继续执行!");
}
运行结果:
- 如果异常之间具有父子关系,一定是子类异常在前 catch,父类异常在后 catch,否则语法错误
代码示例:
public static void main(String[] args) {
try {
System.out.println(10 / 0);
}catch(Exception e) { //永远都可以捕获到
//快速地定位异常出现的位置
e.printStackTrace();
}catch (ArithmeticException e) {
e.printStackTrace();
}
System.out.println("程序继续执行!");
}
运行结果:
- 可以通过一个 catch 捕获所有的异常,即多个异常,一次捕获(不推荐)
由于 Exception 类是所有异常类的父类。因此可以用这个类型表示捕捉所有异常
2.异常声明throws
处在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常
语法格式:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{
}
比如之前的 clone() 方法:
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
注意事项:
- throws 必须跟在方法的参数列表之后
- 声明的异常必须是 Exception 或者 Exception 的子类
- 方法内部如果抛出了多个异常,throws 之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可
- 调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出
3.finally
在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的
语法格式:
try{
//可能会发生的异常
}catch() {
//对捕获到的异常进行处理
}finally{
//此处的语句无论是否发生异常,都会被执行到
}
代码示例:
//try-catch-fianlly使用方法一
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try{
int x = scanner.nextInt();
System.out.println(x);
}catch(InputMismatchException e) {
e.printStackTrace();
System.out.println("输入的数据不匹配!");
}finally {
System.out.println("finally里的代码被执行!");
scanner.close();
}
}
//try-catch-fianlly使用方法二
//直接在 try 后面括号加上以后可能需要关闭的代码,当抛出异常时,将自动关闭
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
int x = scanner.nextInt();
System.out.println(x);
} catch (InputMismatchException e) {
e.printStackTrace();
System.out.println("输入的数据不匹配!");
} finally {
System.out.println("finally里的代码被执行!");
}
}
运行结果:
问题一:既然 finally 和 try-catch-finally 后的代码都会执行,那为什么还要有 finally 呢?
代码示例:
public class TestFinally {
public static int getData() {
Scanner sc = null;
try {
sc = new Scanner(System.in);
int data = sc.nextInt();
return data;
} catch (InputMismatchException e) {
e.printStackTrace();
} finally {
System.out.println("finally中代码");
}
System.out.println("try-catch-finally之后代码");
if (null != sc) {
sc.close();
}
return 0;
}
public static void main(String[] args) {
int data = getData();
System.out.println(data);
}
}
程序正常执行时:
程序抛出异常时:
两者对比可以看出,当程序正常执行时,try-catch-finally之后的代码将不会被执行,而是会直接返回,但 finally 的代码一定会被执行,因此,如果想要程序或方法结束时,某些东西被关闭,则尽量放在 finally
问题二:当遇到下列代码,getData 方法会返回什么?
public static int getData() {
try (Scanner sc = new Scanner(System.in)) {
int data = sc.nextInt();
return data;
} catch (InputMismatchException e) {
e.printStackTrace();
} finally {
return 100;
}
}
运行结果:
由此我们可以看出,绝对不要在 finally 内返回数据,否则返回的数据永远是 finally 里的,其他地方返回的数据都是无效的
(四)程序处理流程
- 程序先执行 try 中的代码
- 如果 try 中的代码出现异常,就会结束 try 中的代码,看和 catch 中的异常类型是否匹配
- 如果找到匹配的异常类型,就会执行 catch 中的代码
- 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者
- 无论是否找到匹配的异常类型,finally 中的代码都会被执行到(在该方法结束前执行)
- 如果上层调用者也没有处理了异常,就继续向上传递
- 一直到 main 方法也没有合适的代码处理异常,就会交给 JVM 来进行处理,此时程序就会异常终止
三、自定义异常类
Java 中虽然已经内置了丰富的异常类, 但是并不能完全表示实际开发中所遇到的一些异常,此时就需要维护符合我们实际情况的异常结构
例如,我们实现一个用户登录功能
public class LogIn {
private String userName = "admin";
private String password = "123456#@";
public void loginInfo(String userName, String password) {
if (!this.userName.equals(userName)) {
System.out.println("用户名错误!");
return;
} if (!password.equals(password)) {
System.out.println("密码错误!");
return;
}
System.out.println("登陆成功");
}
public static void main(String[] args) {
LogIn logIn = new LogIn();
logIn.loginInfo("admin", "12345");
}
}
此时我们在处理用户名密码错误的时候可能就需要抛出两种异常,我们可以基于已有的异常类进行扩展(继承),创建和我们业务相关的异常类
具体方式:
- 自定义异常类,然后继承自 Exception 或者 RunTimeException
- 实现一个带有 String 类型参数的构造方法,参数含义:出现异常的原因
代码实现:
非受查异常定义:
public class PassWordErrorException extends RuntimeException {
public PassWordErrorException() {
super();
}
public PassWordErrorException(String s) {
super(s);
}
}
public class UserNameErrorException extends RuntimeException{
public UserNameErrorException() {
super();
}
public UserNameErrorException(String s) {
super(s);
}
}
public void loginInfo(String userName, String password) {
try {
if (!this.userName.equals(userName)) {
throw new UserNameErrorException("用户名错误!");
} if (!this.password.equals(password)) {
throw new PassWordErrorException("密码错误!");
}
System.out.println("登陆成功");
}catch(UserNameErrorException | PassWordErrorException e) {
e.printStackTrace();
}
}
受查异常定义:
//继承 Exception 类就默认定义的是受查异常
public class PassWordErrorException extends Exception {
public PassWordErrorException() {
super();
}
public PassWordErrorException(String s) {
super(s);
}
}
public class UserNameErrorException extends Exception{
public UserNameErrorException() {
super();
}
public UserNameErrorException(String s) {
super(s);
}
}
//受查情况抛出异常
//如果不在方法内处理,则必须声明
public void loginInfo(String userName, String password) throws UserNameErrorException,PassWordErrorException{
if (!this.userName.equals(userName)) {
throw new UserNameErrorException("用户名错误!");
} if (!this.password.equals(password)) {
throw new PassWordErrorException("密码错误!");
}
System.out.println("登陆成功");
}
//如果方法内没有处理,则必须在调用方法的方法中声明或处理
//若到main方法都只是声明,那么最终会交给 JVM
public static void main(String[] args) {
LogIn logIn = new LogIn();
try{
logIn.loginInfo("admin", "12345");
}catch(UserNameErrorException | PassWordErrorException e) {
e.printStackTrace();
}
}
注意事项:
- 自定义异常通常会继承自 Exception 或者 RuntimeException
- 继承自 Exception 的异常默认是受查异常
- 继承自 RuntimeException 的异常默认是非
总结
以上就是今天要讲的内容,异常这一节比较简单,但也是我们编写代码的一大利器,有利于我们迅速地锁定问题并解决