22、Java基础教程之异常捕获处理·上

  • 本节学习目标
  • 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个核心的关键字: trycatchfinally,利用这3个关键字就可以组成以下异常处理代码格式。

try{
   
     
	//有可能出现异常的语句
}[catch(异常类型对象){
   
     
	//异常处理;
} catch(异常类型对象){
   
     
	//异常处理;
) catch(异常类型对象){
   
     
	//异常处理;
}...][finally{
   
     
	//不管是否出现异常,都会统一执行的代码
}]

在格式中明确地表示,在 try 语句中捕获可能出现的异常代码,如果在 try 中产生了异常,则程序会自动跳转到 catch 语句中找到匹配的异常类型进行相应的处理。最后不管程序是否会产生异常, 都会执行到 finally 语句体, finally 语句就作为异常的统一出口。

需要提醒大家的是,finally 块是可以省略的,如果省略了finally 块不写,则在 catch()块运行结束后,程序将继续向下执行。异常的基本处理流程图如下所示。

*

在以上异常处理的语法格式中,可以发现 catchfinally 块都是可选的。实际上这并不是表示这两个语句可以同时消失,异常处理语法格式的组合,往往有3种形式:try...catchtry...catch...finallytry...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中进行异常类子类命名时都会使用 XxxErrorXxxException 的形式,这样也是为了从名称上帮助开发者区分。清楚了类的继承关系后,下面了解一下 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的代码那样,直接抛出一个手工创建异常类的实例化对象,对于异常还是要尽可能回避,而其要想使用,必须结合 trycatchfinallythrows一起完成。

throwthrows 都是在异常处理中使用的关键字,这两个关键字的区别如下。

  • throw:指的是在方法中人为抛出一个异常类对象(这个异常类对象可能是自己实例化或者是抛出已存在的);
  • throws:在方法的声明上使用,表示此方法在调用时必须处理异常。

6️⃣ 异常处理的标准格式

异常处理除了上面介绍过的最为常见的“try...catch” 应用格式外,还存在一种结合“trycatchfinallythrowthrows" 一起使用的异常处理格式。在讲解这一应用之前,首先来看一个简单的开发要求:要求定义一个 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 进行默认处理。

总之,RuntimeExceptionException 的子类,Exception 定义了必须处理的异常,而 RuntimeException 定义的异常可以选择性地进行处理。常见的 RuntimeExceptionNumberFormatExceptionClassCastExceptionNullPointerExceptionArithmetionExceptionArrayIndexOutOfBoundsException等更多异常信息请前往我的专栏进行学习:
《Java专栏——异常体系》

*


[* ]nbsp_nbsp 6