第六章 异常
一、基本概念
1. 异常的定义
Java程序在运行过程中常会出现各种错误,如:
① 用户输入出错
② 所需文件找不到
③ 运行时磁盘空间不够
④ 内存耗尽无法进行类的实例化
⑤ 算术运算错 (数的溢出,被零除…)
⑥ 数组下标越界
⑦ JVM崩溃
…
Java程序在运行过程中出现的错误,称之为异常。如果对异常置之不理,则程序会中断退出,这显然不是人们所希望看到的。Java提供异常处理机制,使得异常发生后程序仍处于可控状态,而非中断退出,从而使程序更健壮。
注意:Java程序在编译期间出现的语法错误可以通过修改程序来解决,这不是异常。
下面看一个简单的例子:
1 | public class ExceptionDemo { |
编译运行后,结果如右图所示:
从键盘输入整数0,当程序执行到输出语句System.out.println("Result2 is: " + 100/a)时,会出现被0除的情况,这是无法执行的运算,会产生一个异常。该异常是算术运算异常(ArithmeticException)。
注意:除以整数0,会引发异常;而除以浮点数0,不会引发异常,结果为无穷大常量:Infinity
在异常处理机制出现之前,为了避免程序中的运行错误,一般使用大量的判断语句避免错误发生,这使得程序比较臃肿。然而,一旦错误实际发生了,程序还是会崩溃退出。
异常处理机制提供了一种具有恢复能力的处理方式。当错误发生时,JVM启动异常处理来执行一些善后处理。当善后处理执行完毕后,程序可以恢复正常运行状态。
Java程序中的每个错误对应一个异常类,当程序在运行过程中产生一个错误时,就生成一个异常对象,该对象封装了和错误相关的信息。
异常处理包括两步:
- 异常抛出
- 异常捕获
当JVM检测到一个运行中的错误时,会在错误发生的位置生成一个对应的异常对象,称为异常抛出。异常抛出后,JVM寻找该异常的处理代码,找到后把异常对象交给该代码去处理,称为异常捕获。如果找不到异常处理代码,JVM就执行默认处理:从错误发生的位置退出程序,并输出异常信息。
2. Java的异常类
针对各种错误,Java类库定义了许多异常类,这些异常类称为标准异常类,基本涵盖了常见的程序错误。
- Throwable类是所有异常类的父类,Throwable类派生出两个子类Error和Exception。
- Error类及其子类所表示的异常是由JVM产生的错误。这类异常不是由用户程序产生的,也不由用户程序处理。如果出现了Error类型的异常,JVM报告错误信息并终止运行。
- Exception类及其子类所表示的异常是由用户程序产生的,需要由用户程序处理。如果没有异常处理代码,则交由JVM执行默认处理。
Throwable类
该类是所有异常类的父类,提供了异常类的公共方法,下面介绍经常用到的几个方法。
- public Throwable()
- 构造一个描述信息为null的异常对象,会调用fillInStackTrace()来记录异常的堆栈跟踪数据。
- public Throwable(String message)
- 构造带描述信息message的异常对象,会调用fillInStackTrace()来记录异常的堆栈跟踪数据。
- public String getMessage()
- 返回异常对象的描述信息。
- public String toString()
- 返回异常对象的描述信息,通常是发生异常的类名再加上getMessage()返回的描述信息。
- public void printStackTrace()
- 输出异常对象的详细信息,包括异常类型、发生原因,发生位置等。
Error类
- Error是Throwable的子类,由系统保留,用户程序不能使用。
- Error类对应着JVM产生的错误,由JVM自行处理。
Exception类
- Exception是Throwable的子类,表示用户程序产生的异常。
- Exception类没有新增的方法,有两个常用的构造方法:public Exception()和public Exception(String s)。
3. 检查异常(checked exception)和非检查异常(unchecked exception)
Exception及其子类对应的异常可分为两类:RuntimeException及其子类对应的异常称为非检查异常,除此以外的异常称为检查异常。编译器会检查程序是否为检查异常提供了异常处理代码(try-catch语句),如果没有,编译器会报错。比如,与文件、IO、SQL、网络等相关的方法往往包含检查异常,调用时必须提供try-catch语句,否则编译不能通过。而对于非检查异常,编译器不检查用户程序是否提供了异常处理代码。
例子:
1 | class TestException{ |
当从数组的下标n<0或n>5时,会产生数组越界异常(ArrayIndexOutOfBoundsException)。
思考:你如何避免发生这种异常?
二、异常抛出
自动抛出
在程序运行过程中,如果出现了可被JVM识别的错误,则JVM会自动生成相应的异常对象。
人为抛出
由用户程序中识别错误并生成异常对象。需要在错误产生位置的前面使用条件语句判断错误是否已发生;如果发生,则使用throw语句抛出异常对象。语法格式可以是以下两者之一:
throw 异常对象;
先创建好异常对象,需要时抛出。尽量不要使用这种方式,因为异常对象的创建位置会被当作错误的发生位置。如下例所示:
1 | class Throw_Exp1 { |
throw new 异常类(…);
在创建异常对象的同时抛出。如下例所示:
1 | class Throw_Exp2 { |
标准异常一般是自动抛出,当然也可以人为抛出。如果是人为抛出,则必须在错误发生位置前抛出,否则JVM会抢先抛出异常。
throws语句
与throw不同,throws并不是抛出异常,而是在定义一个方法时向外界声明该方法内部可能会抛出某些异常,语法格式如下所示:
1 | [修饰符] 返回值类型 方法名(…) throws 异常类1, 异常类2, …{ |
该方法可能抛出两个异常,一个是算术异常(a[n]被0除),一个是数组越界异常(下标n越界)。但不想在该方法内部处理,于是用throws声明抛出,强制其它方法在调用该方法时必须处理这些异常。
三、异常捕获
当有异常抛出时,不管是JVM自动抛出的还是用throw语句人为抛出的,JVM都会寻找该异常的处理代码,然后将异常对象交给处理代码,这就是异常捕获。如果找不到异常处理代码,则由JVM执行默认处理。异常处理代码也就是try-catch语句,完整格式如下所示:
1 | try { |
STEP 1
将可能发生异常的代码放置在try块中。try-catch语句可以在产生异常的当前方法中,也可以在当前方法的调用者中。只要用try块直接或间接包住了发生异常的代码,都可以捕获到异常。
1 | class Try_Catch_Exp1{ |
1 | class Try_Catch_Exp2{ |
STEP 2
如果try块中的代码抛出了异常,程序将中止执行try块中的后续代码,转而将异常对象从前至后逐一的与各catch分支进行匹配,匹配上哪个catch分支就执行哪个,最先匹配上的catch分支会被执行,后面的catch分支不会再匹配。
1 | class Try_Catch_Exp3{ |
在catch块中通常执行错误发生后的善后处理,只是这里为了简单易学,只打印输出异常信息。
STEP 3
finally块是个可选项。无论异常是否发生,只要有finally块,则该块的代码必定会被执行。即使执行到前面try块或catch分支的return语句,也同样是先执行完finally块,然后结束当前方法。无论异常是否发生,都必须执行的代码可以放到finally块中,一般用于资源释放,如流的关闭,数据库连接的关闭等。
1 | class Try_Catch_Exp4{ |
STEP 4
异常对象与catch分支的匹配原则:(1)异常对象是catch分支异常类的实例; (2)异常对象是catch分支异常类的子类的实例。在写catch分支时,应当把子类异常分支放在前面,父类异常分支放在后面,以避免父类分支吸收本应由子类分支处理的异常。
1 | class Try_Catch_Exp5{ |
由于将父类Exception的catch分支放在了前面,所以错误的吸收了本应由子类ArrayIndexOutOfBoundsException的catch分支处理的数组越界异常。
STEP 5
如果匹配上了某个catch分支,则执行完try-catch语句后,程序恢复正常状态,继续运行try-catch语句后面的语句。如果任何catch分支都没匹配上,则先执行finally块(如果有),然后把异常对象抛给当前方法的调用者,如果调用者有异常处理代码(也就是有try-catch语句间接包住了产生异常的代码),那么就执行处理代码,否则继续向上抛出,最终交给 JVM执行默认处理。
1 | class Try_Catch_Exp6{ |
四、自定义异常类
尽管Java提供了很多标准异常类,但并不能穷尽程序运行时所有可能发生的错误,这时候用户可以自己定义错误,并自定义相应的异常类。这类错误不能被JVM识别,因此不能自动抛出异常对象,必须由用户程序用throw语句抛出。
自定义异常类的格式如下所示:
1 | class 自定义异常类名 extends Exception (或Throwable) { |
自定义异常类可以派生自Throwable或Exception类,通常派生自Exception类。自定义异常类通常只需要定义构造方法,在构造方法中一定要使用super语句调用父类带String参数的构造方法,作用是把错误描述告诉父类。
自定义异常对象抛出后,同样可以被JVM所捕获,交给异常处理代码。
1 | import java.util.Scanner; |
上机作业:
前两次作业没自己做完的,继续做。在前两次作业中加上异常处理语句。比如加个除法,除数可能为0(一定要是整数除法);或加个数组访问,下标可能越界。让异常实际发生,看下异常处理代码是否能正确应对。
补充内容: 包
在Java程序开发中,为了便于管理,引入了包机制,将相关或相近的类、接口、枚举等类型组织在同一个包中,成为包的成员,便于查找和使用。每个包是一个名字空间,同一个包中的类型名必须不同,而不同包中的类型名可以相同,即使名字相同,也是相互独立的,可以通过包名加以区别。因此,包可以避免名字冲突。
如同电脑中的文件夹一样,包采用了树形目录的组织方式。一个包除了类、接口、枚举等成员外,还可以包含子包,包名和子包名之间用"."号分隔。例如,cn.cs.test表示包cn的子包cs的子包test。
每个包对应着一个目录,子包对应着子目录。例如,cn.cs.test相当于目录cn\cs\test。
包限定了访问权限,必须拥有包访问权限才能访问某个包中的类型。
1. package语句
Java程序在声明包时,使用package语句。如下例所示:
1 | package cn.cs.test; |
编译后的.class文件存放到cn\cs\test目录下。若包语句改为"package test;",则编译后的.class文件存放到test目录下。
package语句须是Java源文件的第一条有效语句,而且在一个java源文件中,只能有一条package语句。
用javac命令编译一个具有package语句的Java源程序时,需要要使用d选项,如下所示:
1 | javac –d . Example1.java |
“-d"后面的参数用来指定编译结果的存放位置,”.“表示当前路径。整行命令表示编译生成的包存放在当前路径下。当然,编译结果还可以存放到其它路径下,只要将”."替换为其它路径:
1 | javac -d D:\test Example.java |
将编译生成的包存放到D:\test 路径下。
1 | javac -d .\test Example.java |
将编译生成的包存放到当前路径的test子路径下。
用java命令运行有package语句的主类时,必须带包名,如下所示:
1 | java cn.cs.test.Example |
注意:用javac命令编译有package语句的源程序时,d选项必须有,否则只编译生成.class文件,并不生成对应的包目录。
2. import语句
当在Java程序中需要使用一个包的成员时,就要源文件中使用import语句导入该包。在 java源文件中,import语句必须位于package语句之后,所有类的定义之前,可以没有,也可以有多条,其语法格式分两种情况:
- 单类型导入: import 包名.类名; 例如,import java.util.ArrayList;
- 按需类型导入:import 包名.; 例如,import java.util.;
单类型导入是导入包中特定的某个类,所以要指定类名,如下所示;
1 | import java.util. ArrayList; |
而按需类型导入是导入包中的所有类,所以用通配符*表示,如下所示。
1 | import java.util.*; |
按需类型导入只能导入当前包的所有类,而不能导入当前包的子包的所有类。例如,"import java.*;"
是导入java包中的有所类,如果要导入java包的io子包中的所有类,则是"import java.io.*;"
。
- java.lang包中的类很常用,不管有没有写
"import java.lang.*;"
语句,编译器都会自动补上,
Java不同功能的类放在JDK的不同包中,核心类主要放在java包及其子包下,Java扩展的大部分类都放在javax包以及其子包下。下面是常用的一些包。 - java.lang:包含Java语言的核心类,如String、Math、System和Thread类等,使用这个包中的类无须使用import语句导入,系统会自动导入这个包下的所有类。
- java.util:包含Java中大量工具类、集合类等,例如Arrays、List、Set等。
- java.net:包含Java网络编程相关的类和接口。
- java.io:包含了Java输入、输出有关的类和接口。
- java.awt:包含用于构建图形界面(GUI)的相关类和接口。