异常处理

1…1异常简介
  我们在进行程序设计和运行过程中,发生错误是不可避免的。尽管Java语言的设计便于写出整洁、安全的代码,并且程序员也会尽量地去避免错误的发生。但错误的存在仍然不可避免,有时甚至会使程序被迫终止。为此,Java提供了异常处理机制来帮助程序员处理可能出现的错误,保证了程序的健壮性。本章将向读者介绍异常处理的概念以及如何处理异常,如何创建自定义异常类等知识。
  异常是程序中的一些错误,但并不是所有的错误都是异常,并且有些错误是可以避免的。我们思考一下编写Java程序时可能会遇到哪些异常或错误问题?我们把编写、运行Java程序时会遇到的错误问题分为三种:语法错误、逻辑错误和运行时错误。

1.1语法错误
  这种错误在编译程序时就会发现,比如关键字拼写错误,变量定义时违反了命名规则等。相信读者在之前的实践练习中经常会遇到这样的问题。我们先来阅读下面的一段代码,看看你能找出几个语法错误?

1.1  测试语法错误。
public class Example8_1{
String name
public void test(){
Name = "Test_Java";
}
}
细心的读者会发现,上面的一段代码中有两处语法错误:
 public class Example8_1{
      String name
 public void test(){
         Name = "Test_Java";
 }
}

  语法错误也就是编码不符合Java 语法规范,在编写过程中或编译时就可以被发现,导致编译无法通过。编译器在发现语法错误时就会显示错误信息,编译过程也就此终止。上面代码例子经过编译后就会得到如下图8.1所示的错误信息。
在这里插入图片描述
经过编译后的程序是没有语法错误的,也就是说我们可以在编译程序前消灭语法错误。

1.2逻辑错误
  这种错误很少在编写程序时被轻易地发现,因为这种错误不是语法错误,所以编译器无法提示相应的错误信息。我们来阅读下面的一段代码:

1.2  测试逻辑错误,求1~100之和。
public class Example8_2{
public static void main(String args[]){
             int sum = 0;
             for(int i=0;i<100;i++)
                  sum += i;
             System.out.println("1至100之和是:"+sum);
}
}
我们知道1100之和是5050,但是编译并运行上面的程序后,得到的结论却是4950.为何少了100呢?我们来分析一下这个程序:
 public class Example8_2{
public static void main(String args[]){
             int sum = 0;
             for(int i=0;i<100;i++)
                 sum += i;
             System.out.println("1至100之和是:"+sum);
}
}

原来是循环条件写错了,让循环体少运行了一遍,因此少加了100。我们把循环条件改为:

for(int i=0;i<=100;i++)
修改后再编译运行,结果就正确了。
  逻辑错误,也就是程序设计中常说的Bug,一般存在逻辑错误的程序都是可以顺利的被编译器编译通过的,并且也能够顺利运行,但是得出的结果却并不是我们所希望的。这种错误也是编程新手会经常出现的错误。我们可以通过不断地实践,积累更多的编程经验,来减少这样的错误发生。
8.1.3运行时错误

有些程序虽然编译通过了,但是在运行过程中却出现了问题而导致程序异常终止,出现了运行时的错误。异常就是运行期间出现的错误,而不是编译时的语法错误和逻辑错误。

例如,打开一个不存在的文件、网络连接中断和操作数组越界等,如下图8.2所示。
在这里插入图片描述

我们来编译、运行下面的一段代码:
例1.3 测试运行时错误。
public class Example8_3{
public static void main(String args[]){
int a = 0;
int b = 100/a;
System.out.println(“除法结束。”);
}
}
程序的运行结果如下图8.3所示。
在这里插入图片描述

  在结果中显示出,在程序运行时出现了错误,程序终止了运行,因此第5行的代码没有执行输出。根据异常信息显示,该异常叫“ArithmeticException”,异常的消息是“用零做除数(/ by zero)”,出现异常的位置是第4行。像ArithmeticException这种错误就属于Java的异常,也就是说Java中的异常是一种运行时的错误,会导致程序异常终止,同时显示异常信息。

1.4  数组下标越界。
public class ArrayException {
    public static void main(String args[]) {
        int i = 0;
String greetings[]={"Hello World","Hello Dingdang","Hello Kitty" };
        while (i < 4) {
            System.out.println(greetings[i]);
            i++;
        }
    }
}
程序运行结果如下图8.4所示。

在这里插入图片描述在结果中显示出,在程序运行时出现了错误,程序终止了运行。根据异常信息显示,该异常叫“java.lang.ArrayIndexOutOfBoundsException”,异常的消息是“数组下标越界”, 数组下标越界这样的错误也是Java异常中的一种,出现异常的位置是第6行,出现此异常的原因是数组中共三个元素,下标分别是0、1、2,而循环中代码会访问数组中下标为3的元素,所以导致出现异常。

1.2异常类的继承关系

1.3异常的继承树

异常是程序执行的时候所遇到的非正常情况或意外行为。一般情况下,如数组下标越界、算法溢出、除数为零等都可以引发异常发生。所有异常类型都是Throwable的子类,Throwable处在异常类层次结构的顶层。它是所有异常类的父类。它有两个子类,即Error 和Exception,它们分别用来处理两组异常,如下图8.5所示。
在这里插入图片描述异常类的层次结构如下图所示。
在这里插入图片描述
Object 类的直接子类Throwable描述了所有被虚拟机抛出的非正常状况。一般情况下很少用Throwable,而是使用它的两个子类Error、Exception。

Error类特指应用程序在运行期间发生的严重错误。如:虚拟机内存用尽、堆栈溢出等等。一般情况下这种错误都是灾难性的,所以没有必要使用异常处理机制处理Error。

Exception类有几十个子类,描述了不同类型的异常,其中:

  以RuntimeException为代表的一些类,称为非检查性异常(unchecked Exception),用来表示设计或实现方面的问题,如数组下标越界、除数为零等问题。因为设计和实现正确的程序不会引发这类异常,所以Java并不强制提前处理它。发生这类异常时,运行时环境会输出一条信息,提示用户如何去修正错误。我们可以利用Java提供的异常处理机制,对可能出现的异常进行预见性处理,以保证程序的顺利运行而不是异常终止。

  以IOException为代表的一些类为检查性异常(checked Exception)。所谓的检查和非检查是指编译器在编译时是否检查。如果代码中存在检查性异常,必须进行异常处理,否则编译时不能通过;而非检查性异常编译时不进行检查,到运行时才会显现。因为用户的疏忽很可能导致这类问题在程序运行时发生,比如用户键入的内容不完整等,所以Java鼓励程序员提前处理它们。

2.2 异常类型

异常按照处理方式可以分为如下两种类型:

1.检查性异常(checked exception)

若系统运行时可能产生该类异常,则必须写出相应的处理代码,否则无法通过编译。

2.非检查性异常(unchecked exception)

非检查性异常也称之为RuntimeException,若系统运行时可能产生该类异常,则不必在程序中声明对该类异常的处理,就可以编译执行。

2.2 异常类型

异常按照处理方式可以分为如下两种类型:

1.检查性异常(checked exception)

若系统运行时可能产生该类异常,则必须写出相应的处理代码,否则无法通过编译。

2.非检查性异常(unchecked exception)

非检查性异常也称之为RuntimeException,若系统运行时可能产生该类异常,则不必在程序中声明对该类异常的处理,就可以编译执行。

8.5  异常的类型
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class CheckException {
    public static void main(String args[]) {
        // 检查性异常,需要在程序中进行处理
        FileInputStream fin = new FileInputStream("d://a.txt");
        System.out.println("这里抛出的是一个检查性的错误");
        // 非检查性异常,可以不在程序中进行处理
        String str[] = { "bananan", "apple", "pear" };
        int i = 5;
        System.out.println(str[5]);
    }
}

  一般我们不在代码中处理非检查性异常,这类异常都在运行时抛出。原因主要是由于程序员经验不足或是思维不缜密造成。如:数组越界异常(ArrayIndexOutOfBoundsException)、整数除以0异常(ArithmeticException)等,这类异常其实就是我们通常说的bug。所以,这类异常应通过反复测试尽量避免,而不应该靠异常处理机制来解决。

  检查性异常不同,就算程序员再有经验,也是难以避免。如:用户连接数据库的异常(SQLException),如果是由于数据库服务器没有启动或是网络中断引起的,我们程序员是无法避免的。又如:程序要读取光盘中的文件,而用户忘记插入光盘,此时则抛出文件没找到异常(FileNotFoundException),程序员也无法避免。

综上,异常处理机制主要处理检查性异常而不是非检查性异常和Error。

Java中常见的异常类参见下表所示。
在这里插入图片描述在这里插入图片描述2.3 异常是如何产生的

Java语言是一种面向对象的编程语言,因此,在Java语言中异常也是作为某种异常类的实例的形式出现的。当在某一方法中发生错误时,运行环境就创建一个异常对象,并且把它传递给系统,我们可以利用Java提供的异常处理机制,捕获这个异常对象,并且可以脱离主程序流程进行处理。我们不但可以捕获系统创建的异常实例,还可以创建自己的异常实例,并可以主动抛出异常实例,达到控制程序执行顺序的作用。对于异常的捕获、抛出和创建,将在后面详细讲解。

8.3异常处理机制


8.3.1异常处理过程

在Java程序执行过程中如果出现异常事件,系统会发出异常报告,这时系统将生成一个异常类对象,异常类对象封装了异常事件的信息并将其提交给Java运行时系统

Java 中可用于处理异常的两种方式:

自行处理:可能引发异常的语句封入在 try 块内,而处理异常的相应语句则封入在 catch 块内。

回避异常:在方法声明中包含 throws 子句,通知潜在调用者,该方法可能会出现异常,但没有处理这种异常,必须由调用者处理。异常处理过程如下图8.7所示。

在这里插入图片描述
3.2 使用try/catch 字句
  使用Java提供的异常捕获结构,我们就可以捕获到可能出现的异常,避免程序异常终止。Java的异常捕获结构由try/catch/finally子句组成,我们先来讲讲try/catch子句。try/catch的用法如下图所示。
在这里插入图片描述

例  捕获异常实例,使程序可以成功运行。
public class Exception_Sample_1 {
    public static void main(String args[]) {
        int num[] = new int[5];
        int i = 0;
        int j = 2;
        try {
            while (i < 6) {
                j = j - 1;
                num[i] = i;
                System.out.print(num[i] + "/" + j + "=");
                System.out.println(num[i] / j);
                i++;
            }
        } catch (Exception e2) {
            System.out.println("除数不能是0");
            }
    }
}
运行结果结果如下 
0/1=0
1/0=除数不能是0

运行例题,没有显示异常信息,可以顺利运行。程序运行时由catch子句捕获到这个异常实例,开始执行catch子句中的代码,并且不会回到try子句中了。也就是说在try子句中,在发生异常的语句后面的代码将不再被执行,而是跳转到相应的catch子句中继续执行。

一般情况下我们如下使用try/catch块:

try{

我们把可能会出现异常的语句放在这里。

在异常发生时,会由catch块捕获到相应的异常实例。

}catch(XXXException e){

e就是捕获到的异常实例,它由系统自动创建,里面包含了异常的信息。

XXXException就是Exception异常的任意子类。

/我们在这里可以对出现异常的情况进行处理,比如输出错误信息。

}

注意:

1.catch块,是用来捕获并处理try块抛出的异常的代码块。没有try块,catch块不能单独存在。我们可以有多个catch块,以捕获不同类型的异常

2.如果程序抛出多个不同类型的异常,我们需要多个catch()语句来处理。

3.和特殊异常类相关联的catch()块必须写在和普通异常类相关联的catch()之前。

4.try{…}和catch( ){…}之间不可以添加任何代码

3.3 多重catch子句

  有时一段程序可能会产生多种异常,这时,我们就可以设置多个catch子句来进行捕获,每个子句都来捕获不同类型的异常。当有异常发生时,每个catch子句依次被匹配检查,当发生的异常是catch子句所声明的类型或其子类型时,程序执行流程就转入到该catch子句中继续执行,其余的catch子句将不再检查或执行。多catch子句的用法如下图所示。
在这里插入图片描述每次try块有异常抛出,系统会试着从上到下往每个catch块中传参,直到遇到一个类型匹配的catch块为止。

如上示例中最后一个catch块中的形参为Exception类型,它是所有异常类的父类,任何异常都可以传参到该块中,该块可以处理任何类型的异常。因此,这个catch块只能放到最后面,否则所有的异常都被它处理了,其他的catch块就不能起作用了。

如果编写过程中我们违背了这一点,会产生编译错误:

exception java.io.ArrayOutOfBoundsException has already bean caught

一般一个catch块中是专门处理异常的代码,在程序中这里还可以是记录日志的语句,当发生异常时记录该日志,无异常时将不会记录。

8.7  使用多重catch子句,捕获不同的异常。
import java.io.IOException;
public class Exception_sample_2 {
    public static void main(String args[]) {
        int num[] = new int[5];
        int i = 0;
        int j = 3;// j=8
        try {
            while (i < 6) {
                j = j - 1;
                num[i] = i;
                System.out.print(num[i] + "/" + j + "=");
                System.out.println(num[i] / j);
                i++;
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("数组越界异常");
        } catch (RuntimeException e) {
            System.out.println("数组越界异常");
        }
    }
}
变量j为3时的运行结果
0/2=0
1/1=1
2/0=运行时异常
变量j为8时的运行结果如下
  0/7=0
  1/6=0
  2/5=0
  3/4=0
  4/3=1
  数组越界异常

上面程序运行时,当j的值是3时,出现“被0除”的异常,因此会越过第一个catch子句,执行第二个catch;当j的值为8时,会在访问参数数组时,发生ArrayIndexOutOfBoundsException异常,因此执行第1个catch子句。

思考:

  如果有一个异常类型及其子类型都在各自的catch块中,那么应该把哪个catch块放在前面?
  如果有捕获Exception异常类的catch块,那么这个catch块应该放在其他catch块的前面还是最后面?

3.4finally子句

 从前面的内容中,我们了解到,当try子句中的某行代码发生异常时,会终止程序的运行,跳转到catch子句来执行。但是有些时候,为了程序的健壮性和完整性,无论有没有异常发生都要执行某些代码,比如说打开了文件的I/O流需要关闭,建立的数据库连接需要释放等操作。finally子句就是为了完成必须执行的代码而设计的。

 finally语句放在try …catch语句后,fianlly语句中的代码块不管异常是否被捕获总是要执行,具体运行情况如下图所示。
在这里插入图片描述
通常在finally语句中可以进行资源的释放操作,如:关闭打开文件、删除临时文件。

对应finally代码中的语句,即使try代码块和catch代码块中使用了return语句退出当前方法或应用break跳出某个循环,但程序中的finally代码块都会执行。

当try或catch代码块中执行了System.exit(0)时,finally代码块中的内容不被执行,finally的运行情况如下图所示。
在这里插入图片描述

8.8  使用finally子句。
public class Exception_sample_3 {
    public static void main(String args[]) {
        int num[] = new int[5];
        int i = 0;
        int j = 3;
        try {
            while (i < 3) {
                j = j - 1;
                num[i] = i;
                System.out.println();
                System.out.print(num[i] + "/" + j + "=");
                System.out.println(num[i] / j);
                i++;
            }
        } catch (ArrayIndexOutOfBoundsException e1) {
            System.out.println("数组下标越界");
        } catch (Exception e2) {
            System.out.println("被0除");
            // return;
            // System.exit(0);
        } finally {
            System.out.println("while 执行" + (i + 1) + "次");
        }
        System.out.println("程序结束");
    }
}
程序运行结果如下
    0/2=0
    1/1=1
    2/0=0while 执行3次
    程序结束

由上面两种运行结果来看,无论有无异常发生或者发生了何种异常,最后都会执行finally子句。
3.5throws关键字

  如果一个方法中发生了异常而没有捕获它,那么必须在这个方法头进行声明,由方法的调用者来进行处理。声明异常使用throws关键字。我们来看看下面的代码。

如果一个方法中的语句执行时可能发成某种异常,但是并不能确定如何处理,则可以在程序所在的方法声明后,使用throws关键字抛出异常

位置:方法参数列表的后面

throws关键字后面,可以跟多个异常,中间用逗号分割

throws关键字抛出的异常,由调用该方法的方法处理。throws的用法如下图所示。
在这里插入图片描述
用throws声明方法抛出异常,不进行处理。谁调用谁负责处理。覆盖方法抛出异常时,可以抛出与被覆盖方法相同的异常或者被覆盖方法异常的子类异常。throws在程序中的用法如下图所示。
在这里插入图片描述

8.9  使用throws子句。
public class Exception_Sample_4 {
    public static void devide(int j) throws ArrayIndexOutOfBoundsException,
            RuntimeException {
        int[] num = new int[5];
        int i = 0;

        while (i < 6) {
            j = j - 1;
            num[i] = i;
            System.out.print(num[i] + "/" + j + "=");
            System.out.println(num[i] / j);
            i++;
        }
    }
    public static void main(String args[]) {
        System.out.println("ok");
        try {
            devide(3);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println();
        } catch (RuntimeException e) {
            System.out.println();
        }
    }
}
程序运行结果如下
ok
0/2=0
1/1=1
2/0=
8.10  在方法头声明异常。
public class Example_throws {
    public void test1(int i) throws ArithmeticException {
        System.out.println(100 / i);
    }
    public void test2() {
        try {
            test1(0);
        } catch (ArithmeticException e1) {
            System.out.println(e1.toString());
        }
    }
}

在上面的例题中,方法test1()是被调用者,方法test2()是调用者,在方法test1()中可能会发生“除数为0”的异常,但是并没有对其捕获处理,而是在方法头中声明,这样调用者test2()就必须对这个可能的异常进行处理,要么捕获要么继续声明。

如上所述,我们不建议一味地使用声明异常,在最终的调用者里我们还是应该对异常进行捕获处理,否则就和没有处理异常是一样的效果了。

在Java的语法中,如果一个方法中调用了已经声明检查性异常的另一个方法,那么Java编译器会强制调用者必须处理被声明的异常,要么捕获要么继续声明。

3.6throw 语句

我们在上面的代码例子中,都是在处理系统所发生的异常,其实Java为我们提供了自己产生异常的机会—throw抛出异常。要区分声明异常throws和抛出异常throw两个不同的关键字。抛出异常的语法如下:

throw 异常实例;

异常是通过关键字 throw 抛出,程序可以用throw语句引发明确的异常。如下图所示。
在这里插入图片描述1.4 方法调用堆栈

后进先出:方法A调用方法B的时候,只有方法B先完成后,方法A才完成。先执行的方法总是后完成,后执行的方法先完成,类似于数据结构中的堆栈--后进先出,我们称之为方法调用堆栈。

如示例,仅有一条语句抛出异常,会导致所有的方法都不能正常结束。如果不想出现这样的情况,我们就要使用java的异常处理机制-抓抛模型,方法调用的堆栈如下图8.21所示。
在这里插入图片描述
方法调用的过程如下图:
在这里插入图片描述
【总结与提示】

1.catch子句的捕获范围,限制于与其匹配的try子句,不能捕获其他try子句中的异常;

2.不能单独使用try子句,它必须和catch子句或finally子句结合使用,catch子句可以设置多个,finally子句只能有一个;

3.有多重catch子句时,只能执行那个捕获到异常的子句,其余的catch子句不能执行;

4.try/catch/finally三个子句中变量的作用域独立而不能相互访问。如果要在三个子句中都可以访问,则需要将变量定义到这些子句的外面;

5.不要写过大的try子句,一个try子句中尽量不要存在太多的异常;

6.一个方法中如果有发生异常的可能,则可以进行捕获处理,也可以声明由调用者来处理;
7. throw语句后不允许有其他语句,因为这些语句没有机会执行;

8.不能利用异常处理来进行程序的分支处理,它只是处理非正常情况的一种机制。

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值