- 本节学习目标
- 1️⃣ 异常的概念
- 2️⃣ 处理异常
-
- 2.1 try-catch
- 2.2 try-catch-finally
- 3️⃣ 异常的处理流程
- 4️⃣ throws 关键字
- 5️⃣ throw 关键字
- 6️⃣ 异常处理的标准格式
- 7️⃣ RuntimeException类
本节学习目标
- 了解异常产生的原理;
- 掌握异常处理语句的基本格式语法;
- 掌握 throw、throws 关键字的作用;
- 了解 Exception 与 RuntimeException 的区别;
1️⃣ 异常的概念
在Java 中程序的错误主要是语法错误、语义错误。 一个程序即使在编译时没有错误信息产生,但在运行时有可能出现由于各种各样错误导致的程序退出,那么这些错误在 Java 中统一被称为异常。 Java 对异常的处理提供了非常方便的操作,本章将介绍异常的基本概念以及相关的处理方式。
异常是程序中导致程序中断的一种指令流,为了帮助大家更好地理解异常出现时所带来的问题,下面通过两个案例程序来进行异常产生问题的介绍。
// 范例 1: 不产生异常的代码
package com.xiaoshan.demo;
public class TestDemo {
public static void main(String args[]){
System.out.println("1.除法计算开始。");
System.out.println("2.除法计算:"+(10/2));
System.out.println("3.除法计算结束。");
}
}
程序执行结果:
1.除法计算开始。
2.除法计算:5
3.除法计算结束。
此程序中并没有产生任何异常,所以代码将从头到尾,按照顺序执行完毕。下面对程序进行改写,观察程序中产生异常会带来的问题。
// 范例 2: 产生异常
package com.xiaoshan.demo;
public class TestDemo {
public static void main(String args[]){
System.out.println("1.除法计算开始。");
System.out.println("2.除法计算:"+(10/0)); // 此处产生异常
System.out.println("3.除法计算结束。"); //出现异常并且没有正确处理后,异常之后的语句将不再执行
}
}
程序执行结果:
1.除法计算开始。
Exception in thread "main"java.lang.ArithmeticException: / by zero at com.xiaoshan.demo.TestDemo.main(TestDemo.java:6)
此程序中 由于10/0
运算中除数为零,产生了“ArithmeticException
” 数学运算异常,由于程序没有进行异常的任何处理,所以默认情况下,会进行异常信息打印,同时将终止执行异常产生之后的代码。
通过观察可以发现,如果没有正确地处理异常,程序会出现中断执行的情况。为了让程序在出现异常后依然可以正常执行完毕,必须引入异常处理语句来完善代码编写。
2️⃣ 处理异常
Java 针对异常的处理提供了3个核心的关键字: try
、catch
、finally
,利用这3个关键字就可以组成以下异常处理代码格式。
try{
//有可能出现异常的语句
}[catch(异常类型对象){
//异常处理;
} catch(异常类型对象){
//异常处理;
) catch(异常类型对象){
//异常处理;
}...][finally{
//不管是否出现异常,都会统一执行的代码
}]
在格式中明确地表示,在 try
语句中捕获可能出现的异常代码,如果在 try
中产生了异常,则程序会自动跳转到 catch
语句中找到匹配的异常类型进行相应的处理。最后不管程序是否会产生异常, 都会执行到 finally
语句体, finally
语句就作为异常的统一出口。
需要提醒大家的是,finally
块是可以省略的,如果省略了finally
块不写,则在 catch()
块运行结束后,程序将继续向下执行。异常的基本处理流程图如下所示。
在以上异常处理的语法格式中,可以发现 catch
与 finally
块都是可选的。实际上这并不是表示这两个语句可以同时消失,异常处理语法格式的组合,往往有3种形式:try...catch
、try...catch...finally
、try...finally
。
2.1 try-catch
// 范例 3: 应用异常处理格式
package com.xiaoshan.demo;
public class TestDemo{
public static void main(String args[]){
System.out.println("1.除法计算开始。");
try{
System.out.println("2.除法计算:"+(10/0)); //此处产生异常
System.out.println("更多文章请访问:https://lst66.blog.csdn.net"); //异常产生之后的语句将不再执行,此处在try中产生异常,所以下面的输出不会执行
}catch (ArithmeticException e){
//处理算术异常
System.out.println("******** 出现异常了*********");
System.out.println("3.除法计算结束。");
}
}
}
程序执行结果:
1.除法计算开始。
******** 出现异常了*********
3.除法计算结束。
此程序使用了异常处理语句格式,当程序中的数学计算出现异常后,异常会被 try
语句捕获,而后交给 catch
进行处理,这时程序会正常结束,而不会出现中断执行的情况。
上边范例在出现异常后,是采用输出提示信息的方式进行处理的,但是这样的处理方式不能够明确地描述出异常类型,而且出现异常的目的是解决异常。所以为了能够进行异常的处理,可以使用异常类中提供的 printStackTrace()
方法进行异常信息的完整输出。
// 范例 4: 输出异常的完整信息
package com.xiaoshan.demo;
public class TestDemo{
public static void main(String args[]){
System.out.println("1.除法计算开始。");
try {
System.out.println("2.除法计算:"+(10/0)); //此处产生异常
//异常产生之后的语句将不再执行,此处在try中产生异常,所以下面的输出不会执行
System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
}catch(ArithmeticException e){
//处理算术异常
e.printStackTrace(); //输出异常的完整信息
}
System.out.println("3.除法计算结束。");
}
}
程序执行结果:
1.除法计算开始。
java.lang.ArithmeticException: / by zero
at com.xiaoshan.demo.TestDemo.main(TestDemo.java:7)
3.除法计算结束。
所有的异常类中都会提供 printStackTrace()
方法,而利用这个方法输出的异常信息,会明确地告诉用户是代码中的第几行出现了异常,这样非常方便用户进行代码的调试。
2.2 try-catch-finally
上边范例的代码演示了 “try...catch
” 语句结构,而除了这样的搭配,也可以使用 “try...catch...finally
" 结构进行处理。
// 范例 5:使用完整异常处理结构
package com.xiaoshan.demo;
public class TestDemo {
public static void main(String args[]){
System.out.println("1.除法计算开始。");
try {
System.out.println("2.除法计算:"+(10/0)); //此处产生异常
//异常产生之后的语句将不再执行,此处在try中产生异常,所以下面的输出不会执行
System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
} catch(ArithmeticException e){
//处理算术异常
e.printStackTrace(); //输出异常的完整信息
} finally{
System.out.println("### 不管是否出现异常我都执行!");
System.out.println("3.除法计算结束。");
}
}
}
程序执行结果:
1.除法计算开始。
java.lang.ArithmeticException: / by zero
at com.xiaoshan.demo.TestDemo.main(TestDemo.java:7)
### 不管是否出现异常我都执行!
3.除法计算结束。
此程序增加了一个 finally
语句,这样在异常处理过程中,不管是否出现异常实际上最终都会执行 finally
语句块中的代码。
在异常捕获时发现一个 try
语句也可以与多个 catch
语句使用,这样就可以捕获更多的异常种类。为了让读者更加清楚捕获多个异常的作用以及问题,下面首先对原程序进行部分修改。
// 范例 6: 修改程序,利用初始化参数传递数学计算数据
package com.xiaoshan.demo;
public class TestDemo {
public static void main(String args[]){
System.out.println("1.除法计算开始。");
try {
int x =Integer.parseInt(args[0]); //接收参数并且转型
int y =Integer.parseInt(args[1]); //接收参数并且转型
System.out.println("2.除法计算:"+(x/y)); //此处产生异常
//异常产生之后的语句将不再执行,此处在try中产生异常,所以下面的输出不会执行
System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
}catch(ArithmeticException e){
//处理算术异常
e.printStackTrace(); //输出异常的完整信息
} finally {
System.out.println("### 不管是否出现异常我都执行!");
}
System.out.println("3.除法计算结束。");
}
}
程序执行结果,会根据初始化参数而分为以下几种情况:
(1)执行时不输入参数 ( java TestDemo
):会产生数组越界异常ArrayIndexOutOfBoundsException
,且该异常未处理;
1.除法计算开始。
Exception in thread "main"java.lang.ArrayIndexOutOfBoundsException: 0
at com.xiaoshan.demo.TestDemo.main(TestDemo.java:7)
### 不管是否出现异常我都执行!
(2)输入参数不是数字 ( java TestDemo a b
):会产生数字格式异常NumberFormatException
,该异常未处理;
1. 除法计算开始。
Exception in thread "main"java.lang.NumberFormatException: For input string: "a"
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Integer.parselnt(Unknown Source)
at java.lang.Integer.parselnt(Unknown Source)
at com.xiaoshan.demo.TestDemo.main(TestDemo.java:7)
###不管是否出现异常我都执行!
(3)被除数为0 ( java TestDemo 10 0
) :会产生数学运算异常ArithmeticException
,但该异常已处理;
1. 除法计算开始。
java.lang.ArithmeticException: / by zero
at com.xiaoshan.demo.TestDemo.main(TestDemo.java:9)
### 不管是否出现异常我都执行!
3. 除法计算结束。
此程序中由于只处理了算术异常 (catch (ArithmeticException e)
),所以当出现其他异常后,程序依然无法处理,也会直接中断执行,但是通过执行也可以发现,此时即使没有处理的异常,finally
块也会正常执行,而其他语句将不再执行。
// 范例 7: 加入多个catch 进行异常处理
package com.xiaoshan.demo;
public class TestDemo{
public static void main(String args[]){
System.out.println("1.除法计算开始。");
try {
int x =Integer.parseInt(args[0]); //接收参数并且转型
int y =Integer.parseInt(args[1]); //接收参数并且转型
System.out.println("2.除法计算:"+(x/y)); //此处产生异常
//异常产生后的语句将不再执行,此处在try中产生异常,所以下面的输出不会执行
System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
} catch(ArrayIndexOutOfBoundsException e){
//处理索引越界异常
e.printStackTrace();
} catch(NumberFormatException e){
//处理数字转换异常
e.printStackTrace();
} catch(ArithmeticException e){
//处理算术异常
e.printStackTrace(); //输出异常的完整信息
} finally{
System.out.println("### 不管是否出现异常我都执行!";
System.out.println("3. 除法计算结束。");
}
}
}
此程序在异常处理中加入了多个 catch
语句,这样就可以处理3种异常,并且这3种异常的处理形式完全相同,都是打印异常信息。
3️⃣ 异常的处理流程
通过上面的分析,相信大家已经清楚如何进行异常处理,了解到了异常处理对于程序正常执行完整逻辑的重要性。但是此时来思考这样一个问题:如果每次处理异常时 都要去考虑所有的异常种类,那么直接使用判断来进行处理不是更好吗?所以为了能够正确地处理异常,就必须清楚异常的继承结构以及处理流程。
为了解释异常的继承结构,首先来观察以下两个异常类的继承关系。
通过这两个异常类可以发现所有的异常类型最高的继承类是 Throwable
, 并且在 Throwable
下有两个子类。
- Error: 指的是JVM 错误,程序无法控制处理;
- Exception: 指的是程序运行中产生的异常,用户可以使用异常处理语句进行处理。
可以发现,在Java中进行异常类子类命名时都会使用 XxxError
或 XxxException
的形式,这样也是为了从名称上帮助开发者区分。清楚了类的继承关系后,下面了解一下 Java 中异常的处理完整流程。
(1)当程序在运行的过程中出现了异常,会由JVM 自动根据异常的类型实例化一个与之类型匹配的 异常类对象(此处用户不用去关心如何实例化对象,由JVM 负责处理);
(2)产生异常对象后会判断当前的语句是否存在异常处理,如果现在没有异常处理,就交给 JVM 进行默认的异常处理,处理的方式:输出异常信息,而后结束程序的调用。
(3)如果此时存在异常的捕获操作,那么会先由 try
语句来捕获产生的异常类实例化对象,再与 try
语句后的每一个 catch
进行比较,如果匹配到符合的异常类型,则使用当前 catch
的语句体来进行异常的处理,如果不匹配,则向下继续匹配其他 catch
块。
(4)不管最后异常处理是否能够匹配,都要向后执行,如果此时程序中存在 finally
语句,就先执行 finally
中的代码。执行完 finally
语句后需要根据之前的 catch
匹配结果来决定如何执行,如果之前已经成功地捕获了异常,就继续执行 finally
之后的代码,如果之前没有成功地捕获异常,就将此异常交给 JVM 进行默认处理(输出异常信息,而后结束程序执行)。
整个过程就好比方法传递参数一样,只是根据 catch
后面的参数类型进行匹配。既然异常捕获只是一个异常类对象的传递过程,那么依据Java 中对象自动向上转型的概念来讲,所有异常类对象都可以向父类对象转型,也就证明所有的异常类对象都可以使用 Exception
来接收,这样就可以简单地实现异常处理了。
// 范例 8: 使用 Exception 处理异常
package com.xiaoshan.demo;
public class TestDemo {
public static void main(String args[]){
System.out.println("1.除法计算开始。");
try{
int x = Integer.parseInt(args[0]); //接收参数并且转型
int y = Integer.parseInt(args[1]);
System.out.println("2.除法计算:"+(x/y)); //此处可能产生异常
//异常产生之后的语句将不再执行,此处在try中产生异常,所以下面的输出不会执行
System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
} catch(Exception e){
//处理所有异常类型
e.printStackTrace();
} finally{
System.out.println("### 不管是否出现异常都执行!");
}
System.out.println("3.除法计算结束。");
}
}
此程序的异常统一使用了 Exception
进行处理,这样不管程序中出现了何种异常问题,程序都可以捕获并处理。
在实际的项目开发工作中,所有的异常是统一使用 Exception
处理还是分开处理,完全由开发者的项目开发标准来决定。如果项目开发环境严谨,基本上都会要求针对每一种异常分别进行处理,并且要详细纪录下异常产生的时间以及产生的位置,这样就可以方便程序维护人员进行代码的维护。
同时,实际上用户所能够处理的大部分异常,Java中的异常体系都已经完整定义好了每一种异常。
*关于Java异常体系更多详细的情况,可以前往我的专栏专门学习: 《Java专栏——异常体系》
需要注意的是,当处理多个异常时,捕获范围小的异常要放在捕获范围大的异常之前处理。比如说项目代码中既要处理 " ArithmeticException
” 异常,也要处理 “Exception
" 异常,那么按照继承的关系来讲, ArithmeticException
一定是 Exception
的子类,所以在 编写异常处理时, Exception
的处理一定要写在 ArithmeticException
处理之后,否则将出现语法错误。
// 范例 9: 错误的异常捕获顺序
package com.xiaoshan.demo;
public class TestDemo {
public static void main(String args[]){
System.out.println("1.除法计算开始。");
try {
int x = Integer.parseInt(args[0]); //接收参数并且转型
int y = Integer.parseInt(args[1]); //接收参数并且转型
System.out.println("2.除法计算:"+(x/y)); // 此处产生异常
//异常产生之后的语句将不再执行,此处在try中产生异常,所以下面输出不会执行
System.out.println("更多文章请访问:https://lst66.blog.csdn.net");
} catch (Exception e){
//处理所有异常类型
e.printStackTrace();
}catch(ArithmeticException e){
// 此处无法处理,Exception已处理完
e.printStackTrace();
} finally {
System.out.println("### 不管是否出现异常都执行!");
}
System.out.println("3.除法计算结束。");
}
}
编译错误提示信息:
TestDemo.java:14:错误:已捕获到异常错误 ArithmeticException
}catch (ArithmeticException e){
//此处无法处理,因为Exception已经处理完了
1个错误
此程序Exception
的捕获范围一定大于ArithmeticException
,所以编写的“catch (ArithmeticException e)
"语句永远不可能被执行到,那么编译就会出现错误。
4️⃣ throws 关键字
thrwos
关键字主要在方法定义上使用,表示此方法中不进行异常的处理,而是交给被调用处处理。
// 范例 10: 使用 throws
class MyMath{
public static int div(int x, int y) throws Exception{
//此方法不处理异常
return x/y;
}
}
此程序定义了一个除法计算操作,但是在 div()
方法上使用了 throws
关键字进行声明,这样就表示在本方法中所产生的任何异常本方法都可以不用处理(如果在方法中处理也允许),而是直接交给程序的被调用处进行处理。由于 div()
方法上存在 throws
抛出的 Exception
异常,则当调用此方法时必须明确地处理可能会出现的异常。
// 范例 11: 调用以上的方法
package com.xiaoshan.demo;
public class TestDemo {
public static void main(String args[]){
try {
//div()方法抛出异常,必须明确进行异常处理
System.out.println(MyMath.dir(10, 2));
}catch (Exception e){
e.printStackTrace();
}
}
}
程序执行结果:
5
此程序由于 MyMath
类的 div()
方法定义上已经明确地抛出了异常,所以调用时必须写上异常处理语句,否则会在编译时出现语法错误。
那么在执行 “MyMath.div(10,2)
” 计算时一定不会出现任何的异常,但是为什么还必须使用异常处理机制呢?
可以换个思路,现在编写的计算操作可能没有问题,但是如果换了另外一个人调用这个方法时,就有可能将被除数设置为0
。正是考虑到了代码的统一性,所以不管调用方法时是否会产生异常,都必须进行异常处理操作。
主方法本身也属于一个Java中的方法,所以在主方法上如果使用了throws
抛出,就表示在主方法里面可以不用强制性地进行异常处理,如果出现了异常,将交给JVM进行默认处理,则此时会导致程序中断执行。
// 范例 12: 在主方法上使用throws
package com.xiaoshan.demo;
public class TestDemo{
public static void main(String args[]) throws Exception {
//表示此异常产生后会直接通过主方法抛出,代码中可以不强制使用异常处理
System.out.println(MyMath.div(10, 0));
}
}
程序执行结果:
Exception in thread "main"java.lang.ArithmeticException: / by zero
at com.xiaoshan.demo.MyMath.div(TestDemo.java:6)
at com.xiaoshan.demo.TestDemo.main(TestDemo.java:12)
此时采用的是JVM中的默认方式对产生的异常进行处理。但是从实际开发来讲,主方法上不建议使用 throws
,因为如果程序出现了错误,也希望其可以正常结束调用。
5️⃣ throw 关键字
之前的所有异常类对象都是由 JVM 自动进行实例化操作的,而现在用户也可以自己手动地抛出一个实例化对象(手工调用异常类的构造方法),此时就需要通过 throw
完成。
// 范例 13: 手工抛出异常
package com.xiaoshan.demo;
public class TestDemo {
public static void main(String args[]){
try{
//直接抛出一个自定义的异常类对象
throw new Exception("自己定义的异常!");
}catch (Exception e){
e.printStackTrace();
}
}
}
程序执行结果:
java.lang.Exception: 自己定义的异常!
at com.xiaoshan.demo.TestDemo.main(TestDemo.java:5)
此程序首先实例化了一个 Exception
异常类对象,然后使用关键字 throw
进行抛出,这时就必须明确进行异常处理。
实际上throw
操作很少像范例 13的代码那样,直接抛出一个手工创建异常类的实例化对象,对于异常还是要尽可能回避,而其要想使用,必须结合 try
、catch
、 finally
、throws
一起完成。
throw
和 throws
都是在异常处理中使用的关键字,这两个关键字的区别如下。
- throw:指的是在方法中人为抛出一个异常类对象(这个异常类对象可能是自己实例化或者是抛出已存在的);
- throws:在方法的声明上使用,表示此方法在调用时必须处理异常。
6️⃣ 异常处理的标准格式
异常处理除了上面介绍过的最为常见的“try...catch
” 应用格式外,还存在一种结合“try
、catch
、finally
、throw
、throws
" 一起使用的异常处理格式。在讲解这一应用之前,首先来看一个简单的开发要求:要求定义一个 div()
方法,而这个方法有以下一些要求。
(1)在进行除法操作之前,输出一行提示信息;
(2)在除法操作执行完毕后,输出一行提示信息;
(3)如果中间产生了异常,则应该交给被调用处来进行处理。
在以上要求中,为了保证计算结束之后可以正常地输出信息,则应该使用 finally
块进行操作;为了保证异常可以交给被调用处使用,应该在方法声明上加上 throws
, 而程序中也不应该处理异常。
// 范例 14: 实现要求
package com.xiaoshan.demo;
class MyMath {
public static int div(int x, int y) throws Exception {
//出现异常要交给被调用处输出
System.out.println("=====计算开始=====");
int result = 0;
try {
result =x/y; //除法计算
}catch (Exception e){
throw e; //向上抛
}finally{
System.out.println("=====计算结束=====");
}
return result;
}
}
public class TestDemo{
public static void main(String args[]){
try {
System.out.println(MyMath.div(10,0));
}catch (Exception e){
e.printStackTrace();
}
}
}
程序执行结果:
=====计算开始=====
=====计算结束=====
java.lang.ArithmeticException: / by zero
at com.xiaoshan.demo.MyMath.div(TestDemo.java:7)
at com.xiaoshan.demo.TestDemo.main(TestDemo.java:19)
此程序的开发已经满足基本的要求,不管是否出现异常,都会将异常交被给调用处输出,同时每次操作都会指定固定的输出。
本次给出的异常处理格式在日后的项目开发之中都会经常使用,可以将以上3个操作,想象为数据库操作:
在进行除法操作之前,输出一行提示信息 = 数据库连接打开;
在除法操作执行完毕后,输出一行提示信息 = 数据库连接关闭;
如果中间产生了异常,则应该交给被调用处来进行处理 = 数据的CRUD操作。
这样的操作在以后的Java EE开发之中也会使用到。
7️⃣ RuntimeException类
在Java 中为了方便用户代码的编写,专门提供了一种 RuntimeException
类 。 这种异常类的最大特征在于:程序在编译时不会强制性地要求用户处理异常,用户可以根据自己的需要有选择性地进行处理,但是如果没有处理又发生了异常了,将交给JVM 默认处理。也就是说 RuntimeException
的子异常类,可以由用户根据需要有选择性地进行处理。
比如,现在要将字符串转换为 int
数据类型,可以利用 Integer
类进行处理,因为在 Integer
类中定义了如下方法。
public static int parseInt(String s) throws NumberFormatException;
此时parselnt()
方法上抛出了一个 NumberFormatException
,而这个异常类就属于 RuntimeException
子类。
所有的RuntimeException
子类对象都可以根据用户的需要进行有选择性的处理,所以调用时不处理也不会有任何编译语法错误。
// 范例 16: 使用parseInt() 方法不处理异常
package com.xiaoshan.demo;
public class TestDemo{
public static void main(String args[]){
int temp = Integer.parseInt("100"); //直接将字符串变为int型
System.out.println(temp);
}
}
程序执行结果:
100
本程序在没有处理 parseInt()
方法声明里的异常的情况下依然实现了正常的编译与运行,但此时一旦出现异常,就将交由JVM 进行默认处理。
总之,RuntimeException
是 Exception
的子类,Exception
定义了必须处理的异常,而 RuntimeException
定义的异常可以选择性地进行处理。常见的 RuntimeException
、NumberFormatException
、ClassCastException
、NullPointerException
、ArithmetionException
、ArrayIndexOutOfBoundsException
等更多异常信息请前往我的专栏进行学习:
《Java专栏——异常体系》
[ ]nbsp_nbsp 6