文章目录
10、异常的重要特性总结
计算机程序的编写也需要考虑处理这些异常情况。异常(exception)是在运行程序时产生的一种异常情况,已经成为了衡量一门语言是否成熟的标准之一。目前的主流编程语言,如 C++、c#、Ruby 和 Python 等大都提供了异常处理机制。
10.1 异常简介
Java 中的异常又称为例外,是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。为了能够及时有效地处理程序中的运行错误,必须使用异常类,这可以让程序具有极好的容错性且更加健壮。
在 Java 中一个异常的产生,主要有如下三种原因:
- Java 内部错误发生异常,Java 虚拟机产生的异常。
- 编写的程序代码中的错误所产生的异常,例如空指针异常、数组越界异常等。
- 通过 throw 语句手动生成的异常,一般用来告知该方法的调用者一些必要信息。
Java 通过面向对象的方法来处理异常。在一个方法的运行过程中,如果发生了异常,则这个方法会产生代表该异常的一个对象,并把它交给运行时的系统,运行时系统寻找相应的代码来处理这一异常。
我们把生成异常对象,并把它提交给运行时系统的过程称为拋出(throw)异常
。运行时系统在方法的调用栈中查找,直到找到能够处理该类型异常的对象,这一个过程称为捕获(catch)异常。
10.2 异常的分类
为了能够及时有效地处理程序中的运行错误,Java 专门引入了异常类。在 Java 中所有异常类型都是内置类 java.lang.Throwable 类的子类,即 Throwable 位于异常类层次结构的顶层。Throwable 类下有两个异常分支 Exception 和 Error,如图所示。
其中异常类 Exception 又分为运行时异常和非运行时异常,这两种异常有很大的区别,也称为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。
- Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类。
- 不检查异常(Unchecked Exception,运行时异常)
- 检查异常(Checked Exception,非运行时异常)
- Error 定义了在通常环境下不希望被程序捕获的异常。一般指的是 JVM 错误,如堆栈溢出。
它们通常是灾难性的致命错误,不是程序可以控制的。
1)运行时异常都是 RuntimeException 类及其子类异常
2)非运行时异常是指 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。
10.3 使用的形式
10.3.1 try{}+catch{}({}不可以省略,可以多个catch)
package exception_use;
import java.io.*;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDat()eFormat;
import java.util.Date;
/**
* 目的:演示多个catch的匹配规则,子类必须在父类之前
* 在多个 catch 代码块的情况下,当一个 catch 代码块捕获到一个异常时,其它的 catch 代码块就不再进行匹配。
*
* 注意:当捕获的多个异常类之间存在父子关系时,捕获异常时一般先捕获子类,再捕获父类。
* 所以子类异常必须在父类异常的前面,否则子类捕获不到。
*/
public class Test2_MoreCatch {
public static void main(String[] args) {
Date date = readDate();
System.out.println("读取的日期 = " + date);
}
public static Date readDate() {
FileInputStream readfile = null;
InputStreamReader ir = null;
BufferedReader in = null;
try {
readfile = new FileInputStream("readme.txt");
ir = new InputStreamReader(readfile);
in = new BufferedReader(ir);
// 读取文件中的一行数据
String str = in.readLine();
if (str == null) {
return null;
}
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date date = df.parse(str);
return date;
} catch (FileNotFoundException e) {
System.out.println("处理FileNotFoundException...");
e.printStackTrace();
} catch (IOException e) {
System.out.println("处理IOException...");
e.printStackTrace();
} catch (ParseException e) {
System.out.println("处理ParseException...");
e.printStackTrace();
}
return null;
}
}
👍总结:
在写异常处理的时候,一定要把异常范围小的放在前面,范围大的放在后面,Exception这个异常的根类一定要刚在最后一个catch里面,如果放在前面或者中间,任何异常都会和Exception匹配的,就会报已捕获到…异常的错误。
10.3.2 try{}+catch{}+final{}({}不能省略,final仅一个,必须执行的)
package exception_use;
import java.util.Scanner;
/**
* 演示try-catch-final的使用——final是必须执行的
*/
public class Test4 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("Windows 系统已启动!");
String[] pros = { "记事本", "计算器", "浏览器" };
try {
// 循环输出pros数组中的元素
for (int i = 0; i < pros.length; i++) {
System.out.println(i + 1 + ":" + pros[i]);
}
System.out.println("是否运行程序:");
String answer = input.next();
if (answer.equals("y")) {
System.out.println("请输入程序编号:");
int no = input.nextInt();
System.out.println("正在运行程序[" + pros[no - 1] + "]");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("谢谢使用!");
}
}
}
10.3.3 try{}+final{}({}不能省略,final仅一个,必须执行的)
package exception_use;
import java.util.Scanner;
/**
* 演示try-final的使用——final是必须执行的
*/
public class Test4 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("Windows 系统已启动!");
String[] pros = { "记事本", "计算器", "浏览器" };
try {
// 循环输出pros数组中的元素
for (int i = 0; i < pros.length; i++) {
System.out.println(i + 1 + ":" + pros[i]);
}
System.out.println("是否运行程序:");
String answer = input.next();
if (answer.equals("y")) {
System.out.println("请输入程序编号:");
int no = input.nextInt();
System.out.println("正在运行程序[" + pros[no - 1] + "]");
}
} finally {
System.out.println("谢谢使用!");
}
}
}
10.4 throws and throw
C++只有throw,C++里边有throw(Exception1,…),java里边没有()。
10.4.1 throws 声明异常
当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法的外部进行处理。使用 throws 声明的方法表示此方法不处理异常。throws 具体格式如下:
returnType method_name(paramList) throws Exception 1,Exception2,…{…}
其中,returnType 表示返回值类型;method_name 表示方法名;paramList 表示参数列表;Exception 1,Exception2,… 表示异常类。
如果有多个异常类,它们之间用逗号分隔。这些异常类可以是方法中调用了可能拋出异常的方法而产生的异常,也可以是方法体中生成并拋出的异常。
使用 throws 声明抛出异常的思路是:当前方法不知道如何处理这种类型的异常,该异常应该由向上一级的调用者处理;如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
10.4.2 throw(手动抛出异常)
与 throws 不同的是,throw 语句用来直接拋出一个异常,后接一个可拋出的异常类对象,其语法格式如下:
throw ExceptionObject;
其中,ExceptionObject 必须是 Throwable 类或其子类的对象。如果是自定义异常类,也必须是 Throwable 的直接或间接子类。
例如,以下语句在编译时将会产生语法错误:
throw new String("拋出异常"); // String类不是Throwable类的子类
当 throw 语句执行时,它后面的语句将不执行,此时程序转向调用者程序,寻找与之相匹配的 catch 语句,执行相应的异常处理程序。如果没有找到相匹配的 catch 语句,则再转向上一层的调用程序。这样逐层向上,直到最外层的异常处理程序终止程序并打印出调用栈情况。
throw 关键字不会单独使用,它的使用完全符合异常的处理机制,但是,一般来讲用户都在避免异常的产生,所以不会手工抛出一个新的异常类的实例,而往往会抛出程序中已经产生的异常类的实例
Demo
在某仓库管理系统中,要求管理员的用户名需要由 8 位以上的字母或者数字组成,不能含有其他的字符。当长度在 8 位以下时拋出异常,并显示异常信息;当字符含有非字母或者数字时,同样拋出异常,显示异常信息。
package exception_use;
import java.util.Scanner;
public class Test5 {
public boolean validateUserName(String username) {
boolean con = false;
if (username.length() > 8) {
// 判断用户名长度是否大于8位
for (int i = 0; i < username.length(); i++) {
char ch = username.charAt(i); // 获取每一位字符
if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
con = true;
} else {
con = false;
throw new IllegalArgumentException("用户名只能由字母和数字组成!");
}
}
} else {
throw new IllegalArgumentException("用户名长度必须大于 8 位!");
}
return con;
}
public static void main(String[] args) {
Test5 te = new Test5();
Scanner input = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = input.next();
try {
boolean con = te.validateUserName(username);
if (con) {
System.out.println("用户名输入正确!");
}
} catch (IllegalArgumentException e) {
System.out.println(e);
}
}
}
10.4.3 区别
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throws 用来声明一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常;throw 则是指拋出的一个具体的异常类型,执行 throw 则一定抛出了某种异常对象。
- 通常在一个方法(类)的声明处通过 throws 声明方法(类)可能拋出的异常信息,而在方法(类)内部通过 throw 声明一个具体的异常信息。
- throws 通常不用显示地捕获异常,可由系统自动将所有捕获的异常信息抛给上级方法; throw 则需要用户自己捕获相关的异常,而后再对其进行相关包装,最后将包装后的异常信息抛出。
10.5 java7的多异常捕抓
如果有些异常种类不同,但捕获之后的处理是相同的,例如以下代码。
try{
// 可能会发生异常的语句
} catch (FileNotFoundException e) {
// 调用方法methodA处理
} catch (IOException e) {
// 调用方法methodA处理
} catch (ParseException e) {
// 调用方法methodA处理
}
3 个不同类型的异常,要求捕获之后的处理都是调用 methodA 方法。
为了解决这种问题,Java 7 推出了多异常捕获技术,可以把这些异常合并处理。
上述代码修改如下:
try{
// 可能会发生异常的语句
} catch (IOException | ParseException e) {
// 调用方法methodA处理
}
注意:由于 FileNotFoundException 属于 IOException 异常,IOException 异常可以捕获它的所有子类异常。所以不能写成 FileNotFoundException | IOException | ParseException 。
使用一个 catch 块捕获多种类型的异常时需要注意如下两个地方。
- 捕获多种类型的异常时,多种异常类型之间用竖线|隔开。
- 捕获多种类型的异常时,异常变量有隐式的 final 修饰,因此程序不能对异常变量重新赋值。
- 这一特性将生成更少的字节码并减少代码冗余。
一个大佬的经验:👍try catch只能捕获统一线程中的异常,如果try catch整个线程,线程中发生崩溃是catch不住的。
10.6 自定义异常
如果 Java 提供的内置异常类型不能满足程序设计的需求,这时我们可以自己设计 Java 类库或框架,其中包括异常类型。实现自定义异常类需要继承 Exception 类或其子类,如果自定义运行时异常类需继承 RuntimeException 类或其子类。
自定义异常的语法形式为:
<class><自定义异常名><extends><Exception>
在编码规范上,一般将自定义异常类的类名命名为 XXXException,其中 XXX 用来代表该异常的作用。
自定义异常类一般包含两个构造方法:
- 一个是无参的默认构造方法
- 另一个构造方法以字符串的形式接收一个定制的异常消息,并将该消息传递给超类的构造方法。
例如,以下代码创建一个名称为 IntegerRangeException 的自定义异常类:
class IntegerRangeException extends Exception {
public IntegerRangeException() {
super();//调用父类的构造方法
}
public IntegerRangeException(String s) {
super(s);
}
}
以上代码创建的自定义异常类 IntegerRangeException 类继承自 Exception 类,在该类中包含两个构造方法。
Demo
例 1
编写一个程序,对会员注册时的年龄进行验证,即检测是否在 0~100 岁。
1)这里创建了一个异常类 MyException,并提供两个构造方法。MyException 类的实现代码如下:
public class MyException extends Exception {
public MyException() {
super();
}
public MyException(String str) {
super(str);
}
}
2)接着创建测试类,调用自定义异常类。代码实现如下:
import java.util.InputMismatchException;
import java.util.Scanner;
public class Test07 {
public static void main(String[] args) {
int age;
Scanner input = new Scanner(System.in);
System.out.println("请输入您的年龄:");
try {
age = input.nextInt(); // 获取年龄
if(age < 0) {
throw new MyException("您输入的年龄为负数!输入有误!");
} else if(age > 100) {
throw new MyException("您输入的年龄大于100!输入有误!");
} else {
System.out.println("您的年龄为:"+age);
}
} catch(InputMismatchException e1) {
System.out.println("输入的年龄不是数字!");
} catch(MyException e2) {
System.out.println(e2.getMessage());
}
}
}
3)运行该程序,当用户输入的年龄为负数时,则拋出 MyException 自定义异常,执行第二个 catch 语句块中的代码,打印出异常信息。程序的运行结果如下所示。
请输入您的年龄:
-2
您输入的年龄为负数!输入有误!
当用户输入的年龄大于 100 时,也会拋出 MyException 自定义异常,同样会执行第二个 catch 语句块中的代码,打印出异常信息,如下所示。
请输入您的年龄:
110
您输入的年龄大于100!输入有误!
10.7 异常跟踪栈
10.7.1 概念
异常跟踪栈
调用 Exception 的 printStackTrace() 方法就是打印该异常的跟踪栈信息
我们先来看一段代码
package exception_use;
public class SelfException extends RuntimeException {
SelfException() {
}
SelfException(String msg) {
super(msg);
}
}
class PrintStackTraceTest {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod() {
secondMethod();
}
public static void secondMethod() {
thirdMethod();
}
public static void thirdMethod() {
throw new SelfException("自定义异常信息");
}
}
运行结果:
图里的内容是异常跟踪栈信息
,从打印的异常信息我们可以看出,异常从 thirdMethod 方法开始触发,传到 secondMethod 方法,再传到 firstMethod 方法,最后传到 main 方法,在 main 方法终止,这个过程就是 Java 的异常跟踪栈。
注意:虽然 printStackTrace() 方法可以很方便地用于追踪异常的发生情况,可以用它来调试程序,但在最后发布的程序中,应该避免使用它。应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。
10.7.2 异常跟踪栈打印了什么?
异常跟踪栈信息的第一行一般详细显示异常的类型和异常的详细消息,接下来是所有异常的发生点,各行显示被调用方法中执行的停止位置,并标明类、类中的方法名、与故障点对应的文件的行。一行行地往下看,跟踪栈总是最内部的被调用方法逐渐上传,直到最外部业务操作的起点,通常就是程序的入口 main 方法或 Thread 类的 run 方法(多线程的情形)。