一、基本概念

1. 异常的定义

Java程序在运行过程中常会出现各种错误,如:
① 用户输入出错

② 所需文件找不到

③ 运行时磁盘空间不够

④ 内存耗尽无法进行类的实例化

⑤ 算术运算错 (数的溢出,被零除…)

⑥ 数组下标越界

⑦ JVM崩溃



Java程序在运行过程中出现的错误,称之为异常。如果对异常置之不理,则程序会中断退出,这显然不是人们所希望看到的。Java提供异常处理机制,使得异常发生后程序仍处于可控状态,而非中断退出,从而使程序更健壮。

注意:Java程序在编译期间出现的语法错误可以通过修改程序来解决,这不是异常。

下面看一个简单的例子:

1
2
3
4
5
6
7
8
9
public class ExceptionDemo {
public static void main(String args[]) {
Scanner sc = new Scanner(System.in);
int a = sc.nextInt();
System.out.println("Result1 is: " + 100/10);
System.out.println("Result2 is: " + 100/a);
System.out.println("Result3 is: " + 100/100);
}
}

编译运行后,结果如右图所示:

从键盘输入整数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
2
3
4
5
6
7
8
9
class TestException{
public static void main(String[] args){
int num[] = new int[]{1, 2, 3, 4, 5, 6};

Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
System.out.println(num[n]);
}
}

当从数组的下标n<0或n>5时,会产生数组越界异常(ArrayIndexOutOfBoundsException)。
思考:你如何避免发生这种异常?

二、异常抛出

自动抛出

在程序运行过程中,如果出现了可被JVM识别的错误,则JVM会自动生成相应的异常对象。

人为抛出

由用户程序中识别错误并生成异常对象。需要在错误产生位置的前面使用条件语句判断错误是否已发生;如果发生,则使用throw语句抛出异常对象。语法格式可以是以下两者之一:

throw 异常对象;

先创建好异常对象,需要时抛出。尽量不要使用这种方式,因为异常对象的创建位置会被当作错误的发生位置。如下例所示:

1
2
3
4
5
6
7
8
9
class Throw_Exp1 {
public void test(int a, int b) {
ArithmeticException e = new ArithmeticException();
if(b == 0) { //错误条件成立则用throw语句抛出异常对象
throw e;
}
System.out.println(a / b);
}
}

throw new 异常类(…);

在创建异常对象的同时抛出。如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Throw_Exp2 {
public void test(int a, int b) {
int c[] = {1, 2, 3, 4, 5};

if(b == 0){ //错误条件成立,则创建异常对象并同时抛出
throw new ArithmeticException();
}
System.out.println(a/b);

if(a > 4 || a < 0){ //错误条件成立,则创建异常对象并同时抛出
throw new ArrayIndexOutOfBoundsException();
}
System.out.println(c[a]);
}
}

标准异常一般是自动抛出,当然也可以人为抛出。如果是人为抛出,则必须在错误发生位置前抛出,否则JVM会抢先抛出异常。

throws语句

与throw不同,throws并不是抛出异常,而是在定义一个方法时向外界声明该方法内部可能会抛出某些异常,语法格式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[修饰符]  返回值类型  方法名(…)  throws  异常类1, 异常类2, …{
...
}
```
如果一个方法使用throws向外界声明抛出异常,则该方法的调用者必须提供这些异常的处理代码(try-catch语句),否则编译不通过。如果调用者也不想处理这些异常,则必须在自身定义时也用throws声明抛出这些异常,否则编译不通过。
例如,方法f()使用throws向外界声明抛出算术异常ArithmeticException,方法g()调用方法f(),则方法g()要么提供算术异常的处理代码,要么在方法g()定义时,也用throws向外界声明抛出算术异常ArithmeticException。
如果在一个方法内部有`检查异常`,又不想在该方法内部处理,则必须在该方法定义时用throws声明抛出该`检查异常`,否则编译不通过。

```java
class Throws_Exp{
public static void test(int []a, int n) throws ArithmeticException, ArrayIndexOutOfBoundsException{
System.out.println(a[n]/n);
}
}

该方法可能抛出两个异常,一个是算术异常(a[n]被0除),一个是数组越界异常(下标n越界)。但不想在该方法内部处理,于是用throws声明抛出,强制其它方法在调用该方法时必须处理这些异常。

三、异常捕获

当有异常抛出时,不管是JVM自动抛出的还是用throw语句人为抛出的,JVM都会寻找该异常的处理代码,然后将异常对象交给处理代码,这就是异常捕获。如果找不到异常处理代码,则由JVM执行默认处理。异常处理代码也就是try-catch语句,完整格式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
//可能发生异常的代码放置在此块中;
}
catch(异常类1 e1) {
//异常类1的处理代码;
}
...
catch(异常类n en) {
//异常类n的处理代码;
}
[finally {
//可选部分。如果有,不论是否发生异常,都一定会执行的部分;
}]

STEP 1

将可能发生异常的代码放置在try块中。try-catch语句可以在产生异常的当前方法中,也可以在当前方法的调用者中。只要用try块直接或间接包住了发生异常的代码,都可以捕获到异常。

1
2
3
4
5
6
7
8
9
class Try_Catch_Exp1{  
void f(int []a, int n) {
try{ //try块直接包住了发生异常的代码
System.out.println(a[n]);
} catch(ArrayIndexOutOfBoundsException e) {
System.out.println(e);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Try_Catch_Exp2{  
void f(int []a, int n) {
System.out.println(a[n]);
}
}

class Test{
void g(){
Throw_Exp exp = new Throw_Exp();
int a[] = {1, 2, 3, 4, 5, 6};
try{ //try块间接包住了发生异常的代码
exp.f(a, 6);
} catch(ArrayIndexOutOfBoundsException e) {
System.out.println(e);
}
}
}

STEP 2

如果try块中的代码抛出了异常,程序将中止执行try块中的后续代码,转而将异常对象从前至后逐一的与各catch分支进行匹配,匹配上哪个catch分支就执行哪个,最先匹配上的catch分支会被执行,后面的catch分支不会再匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Try_Catch_Exp3{
public static void main(String args[]) {
int c[] = {1, 2, 3, 4, 5,6};
try {
System.out.println("请输入整数a: ");
Scanner sc = new Scanner(System.in);
int a = sc.nextInt();
System.out.println(c[a]/a);
} catch(ArithmeticException e) { //输入的a=0,则发生算术异常,匹配该分支
System.out.println("算术异常:" + e.getMessage());
} catch(ArrayIndexOutOfBoundsException e) { //输入的a<0或a>5,则匹配该分支
e.printStackTrace();
}

System.out.println("继续执行try-catch语句后面的语句");
}
}

在catch块中通常执行错误发生后的善后处理,只是这里为了简单易学,只打印输出异常信息。

STEP 3

finally块是个可选项。无论异常是否发生,只要有finally块,则该块的代码必定会被执行。即使执行到前面try块或catch分支的return语句,也同样是先执行完finally块,然后结束当前方法。无论异常是否发生,都必须执行的代码可以放到finally块中,一般用于资源释放,如流的关闭,数据库连接的关闭等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Try_Catch_Exp4{
public static void main(String args[]) {
int c[] = {1, 2, 3, 4, 5,6};
try {
System.out.println("请输入整数a: ");
Scanner sc = new Scanner(System.in);
int a = sc.nextInt();
System.out.println(c[a] / a);
} catch(ArithmeticException e) { //输入的a=0,则发生算术异常,匹配该分支
System.out.println("算术异常:" + e.getMessage());
} catch(ArrayIndexOutOfBoundsException e) { //输入的a<0或a>5,则匹配该分支
e.printStackTrace();
} finally {
System.out.println("这是所有catch块的共有部分,不管异常是否发生,一定会被执行!");
}
}
}

STEP 4

异常对象与catch分支的匹配原则:(1)异常对象是catch分支异常类的实例; (2)异常对象是catch分支异常类的子类的实例。在写catch分支时,应当把子类异常分支放在前面,父类异常分支放在后面,以避免父类分支吸收本应由子类分支处理的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Try_Catch_Exp5{
public static void main(String1args[]) {
int c[] = {1, 2, 3, 4, 5};
try {
System.out.println("请输入整数a: ");
Scanner sc = new Scanner(System.in);
int a = sc.nextInt();
System.out.println(c[a]);
}
catch(Exception e) {
e.printStackTrace();
}
catch(ArrayIndexOutOfBoundsException e) {
e. printStackTrace();
}
}
}

由于将父类Exception的catch分支放在了前面,所以错误的吸收了本应由子类ArrayIndexOutOfBoundsException的catch分支处理的数组越界异常。

STEP 5

如果匹配上了某个catch分支,则执行完try-catch语句后,程序恢复正常状态,继续运行try-catch语句后面的语句。如果任何catch分支都没匹配上,则先执行finally块(如果有),然后把异常对象抛给当前方法的调用者,如果调用者有异常处理代码(也就是有try-catch语句间接包住了产生异常的代码),那么就执行处理代码,否则继续向上抛出,最终交给 JVM执行默认处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Try_Catch_Exp6{
public bool test (int a) {
int c[]={1, 2, 3, 4, 5};
try{
System.out.println (c[a]);
}
catch(ArithmeticException e) {
System.out.println("数组越界异常:" + e);
}
finally{
System.out.println("这是所有catch块的共有部分,一定会执行!");
}
return true;
}
}

四、自定义异常类

尽管Java提供了很多标准异常类,但并不能穷尽程序运行时所有可能发生的错误,这时候用户可以自己定义错误,并自定义相应的异常类。这类错误不能被JVM识别,因此不能自动抛出异常对象,必须由用户程序用throw语句抛出。

自定义异常类的格式如下所示:

1
2
3
class  自定义异常类名 extends Exception (或Throwable) {
异常类体;
}

自定义异常类可以派生自Throwable或Exception类,通常派生自Exception类。自定义异常类通常只需要定义构造方法,在构造方法中一定要使用super语句调用父类带String参数的构造方法,作用是把错误描述告诉父类。
自定义异常对象抛出后,同样可以被JVM所捕获,交给异常处理代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.Scanner;
class Exception_exp{
public static void main(String[] args){
try {
Scanner sc = new Scanner(System.in);
System.out.println("请输入整数a: ");
int a = sc.nextInt();
System.out.println("请输入整数b: ");
int b = sc.nextInt();
sc.close();
System.out.println("a + b = " + add(a, b));
}
catch (UserException e) {
e.printStackTrace();
}
}
static int add(int m, int n) throws UserException { //最好使用thwows语句声明方法的抛出自定义异常
if (m<0 || n<0) throw new UserException("加数为负!");
return n + m;
}
}
class UserException extends Exception{ //根据需要,可以定义一个或两个构造方法。
UserException() { super("数据为负数"); }
UserException(String message) { super(message); }
}

上机作业:
前两次作业没自己做完的,继续做。在前两次作业中加上异常处理语句。比如加个除法,除数可能为0(一定要是整数除法);或加个数组访问,下标可能越界。让异常实际发生,看下异常处理代码是否能正确应对。

补充内容: 包

在Java程序开发中,为了便于管理,引入了包机制,将相关或相近的类、接口、枚举等类型组织在同一个包中,成为包的成员,便于查找和使用。每个包是一个名字空间,同一个包中的类型名必须不同,而不同包中的类型名可以相同,即使名字相同,也是相互独立的,可以通过包名加以区别。因此,包可以避免名字冲突。
如同电脑中的文件夹一样,包采用了树形目录的组织方式。一个包除了类、接口、枚举等成员外,还可以包含子包,包名和子包名之间用"."号分隔。例如,cn.cs.test表示包cn的子包cs的子包test。
每个包对应着一个目录,子包对应着子目录。例如,cn.cs.test相当于目录cn\cs\test。
包限定了访问权限,必须拥有包访问权限才能访问某个包中的类型。

1. package语句

Java程序在声明包时,使用package语句。如下例所示:

1
2
3
4
5
6
package  cn.cs.test;            
public class Example{
public static void main(String args[]){
System.out.println("HelloWorld");
}
}

编译后的.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
2
3
4
5
6
7
import java.util. ArrayList;
public class Test {
public static void main(String[] args) {
ArrayList list = new ArrayList();
...
}
}

而按需类型导入是导入包中的所有类,所以用通配符*表示,如下所示。

1
2
3
4
5
6
7
8
import java.util.*;
public class Test {
public static void main(String[] args) {
ArrayList list1 = new ArrayList();
LinkedList list2 = new LinkedList();
...
}
}

按需类型导入只能导入当前包的所有类,而不能导入当前包的子包的所有类。例如,"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)的相关类和接口。