Java中的异常处理

异常是什么

过程式编程中函数往往用返回特殊的值来表示出现异常情况,比如fread函数,会返回读取的字节数
但是实际编程中程序员可能会忘记检查函数的返回值。这就导致了程序中处理异常情况的缺失。在Java中,编译器会强制让程序员处理程序中出现的某些异常情况。这是基于在Java中当异常发生时,会抛出相应的异常对象,而程序员用捕获这个异常对象来处理异常的机制。

首先来了解下抛出异常对象,下面的代码

   class ExcDemo1 {
        public static void main(String args[]) {
            int nums[] = new int[4];
            nums[7] = 10;
            System.out.println("this won't be displayed");
        }
    }

编译没有错误,但是运行时会让程序崩溃,并显示出现了ArrayIndexOutOfBoundsException异常。ArrayIndexOutOfBoundsException是Java中定义好的一种异常类型,当访问数组元素下标越界时虚拟机JVM会抛出这个异常类型的对象并终止程序的继续运行。如果我们不在程序中处理它,最终虚拟机JVM会捕获它,并显示它里面存储的信息。这里程序运行的结果并没有什么意外,在C语言中出现了这种问题程序也会崩溃,不过在Java中对于运行时产生的异常对象,我们可以用try-catch语句捕获它。

Java中定义好了各种异常类来对应异常情况的发生。

异常类

其中IndexOutOfBoundsExceptionArrayIndexOutOfBoundsExceptionStringIndexOutOfBoundsException的共同父类。

try-catch基础

Java中可以用try-catch语句捕获运行时出现的异常类的对象。

class ExcDemo1 {
    public static void main(String args[]) {
        int nums[] = new int[4];
        try {
            System.out.println("Before exception is generated.");
            // Generate an index out-of-bounds exception.
            nums[7] = 10;
            System.out.println("this won't be displayed");
        }
        catch (ArrayIndexOutOfBoundsException exc) {
            // catch the exception
            System.out.println("Index out-of-bounds!");
        }
        System.out.println("After catch statement.");
    }
}

程序运行结果显示:
Before exception is generated.
Index out-of-bounds!
After catch statement.

当try块里面的语句执行到nums[7] = 10;语句,虚拟机JVM就会抛出相应的ArrayIndexOutOfBoundsException类的异常对象,并将执行流程转移到与异常对象类型相应的catch块中执行,执行完成相当于处理完异常,执行流程来到try-catch之后继续执行,就像没有发生过异常一样。

如果try块中没有发生异常,那么就不会产生异常对象,catch中的代码也不会执行,就像没有try-catch那样。

class ExcDemo1_2 {
    public static void main(String args[]) {
        int nums[] = new int[4];
        try {
            System.out.println("Before exception is generated.");
            // not Generate an index out-of-bounds exception.
            nums[2] = 10;
            System.out.println("this will be displayed");
        }
        catch (ArrayIndexOutOfBoundsException exc) {
            // catch the exception
            System.out.println("Index out-of-bounds!");
        }
        System.out.println("After catch statement.");
    }
}

程序运行结果:
Before exception is generated.
this will be displayed
After catch statement.

catch多个异常

一个try-catch里面可能会抛出多种异常,想要捕获它们,就要提供多个catch来捕获它们。

// Use multiple catch statements.
class ExcDemo3 {
    public static void main(String args[]) {
        // Here, numer is longer than denom.
        int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };
        for(int i=0; i<numer.length; i++) {
            try {
                System.out.println(numer[i] + " / " +
                denom[i] + " is " +
                numer[i]/denom[i]);
            }
            catch (ArithmeticException exc) {
                // catch the exception
                System.out.println("Can't divide by Zero!");
            }
            catch (ArrayIndexOutOfBoundsException exc) {
                // catch the exception
                System.out.println("No matching element found.");
            }
        }
    }
}

程序运行结果:
4 / 2 is 2
Can’t divide by Zero!
16 / 4 is 4
32 / 4 is 8
Can’t divide by Zero!
128 / 8 is 16
No matching element found.
No matching element found.

因为try块中一旦出现某个异常就会跳转到catch中,那么一次异常发生时也只有一个catch会被执行。

异常对象沿着调用函数调用栈反向传递

异常可能发生在被调函数中,如果在该函数中没有try-catch块来捕获相应产生的异常对象,那可以在调用函数中捕获它。

class ExcTest {
    // Generate an exception.
    static void genException() {
        int nums[] = new int[4];
        System.out.println("Before exception is generated.");
        // generate an index out-of-bounds exception
        nums[7] = 10;
        System.out.println("this won't be displayed");
    }
}
class ExcDemo2 {
    public static void main(String args[]) {
        try {
            ExcTest.genException();  // genException()函数中发生异常
            System.out.println("this won't be displayed if genException throw an exception");
        } catch (ArrayIndexOutOfBoundsException exc) {
            // catch the exception
            System.out.println("Index out-of-bounds!");
        }
        System.out.println("After catch statement.");
    }
}

如果有多个函数形成了调用关系,那么异常对象会沿着函数调用栈的反方向尝试被其中的try-catch异常处理块捕获,直到在某个函数中被捕获,异常就在那个函数中得到处理,程序执行流程就沿着那个函数中的try-catch块后的代码继续执行。

异常怎么用

大家可以回忆一下,我们在使用数组的地方也没写过try-catch语句来捕获可能产生的ArrayIndexOutOfBoundsException异常,我们应该怎样利用try-catch异常捕获机制和那些已经定义好的异常类呢?这要从Java对异常类的分类说起。

异常的分类

Throwable是所有异常的根类,它有两个子类Error和Exception。Error类及其子类表示由虚拟机JVM本身的问题产生的异常。这类异常类似电脑本身软/硬件出现错误,程序一般不能也不需要解决。

Exception类及其子类表示程序自己或者运行时环境出现了异常,像访问数组时下标越界产生的ArrayIndexOutOfBoundsException异常和打开和读取文件时可能产生的IOException异常都属于Exception异常的子类。Exception类又可以分为两大部分,一类是以Exception类的子类RuntimeException及RuntimeException的子类组成的异常,一类是其它类型的异常。

RuntimeException称为运行时异常,这个名字有点让人产生混淆,因为所有的异常都是在运行时产生的。RuntimeException的子类包括了数组越界(IndexOutOfBoundsException)、空指针(NullPointerException)、除零(ArithmeticException)等异常类型,它们通常是由虚拟机JVM抛出的。

异常的处理

RuntimeException、Error,以及它们的子类属于非检查异常,其它的异常,即Exception类中除RuntimeException及其子类的异常类,属于检查异常。非检查异常和检查异常,Java对它们有不同的处理要求。

非检查异常

RuntimeException、Error,以及它们的子类属于非检查异常。非检查异常是指编译器不强制程序处理的异常。程序处理异常的是说或者用try-catch进行捕获,或者在所在的方法中不捕获,但是方法要声明可能会抛出这种异常(使用throws语句)。
首先Error及其子类一般是由于JVM的问题而抛出的,一般不需要关注。而从RuntimeException及其子类的类型可以看到,它们基本是由程序的逻辑错误引起的,可能发生的地方太多了,如果在程序需要对它们都要处理,那么程序中处理异常的代码就会满天飞。而且它们一旦发生也很难恢复。因此Java把它们和Error类型的异常归于非检查异常,程序员不需要在程序中假设它们会出现并处理,程序员要做的是写好代码,避免在程序中出现这些异常。比如,写代码的过程中就避免数组下标越界。

检查异常

与非检查异常对应的是检查异常。Exception类及其子类中不属于RuntimeException及其子类的那些类都被归于检查异常,比如在读写文件过程中硬盘损坏导致程序无法读取文件而抛出的IOException类型的异常就是检查异常。检查异常的特点是这些异常的发生往往无法预期,不可能通过编码来避免,因此这类异常编译器强制程序员在写程序的时候要做最坏的打算,即必须进行异常处理。或者用try-catch进行捕获,或者让所在的方法声明(可能会)抛出此类异常,这样调用方法就知道也要进行相应的处理。比如,假设方法p1调用方法p2,而p2可能抛出一个检查异常(例如IOException),那么我们就需要采用下面两种编码方式的任何一种

  • p1捕获异常
void p1(){
    try{
        p2();
    }
    catch(IOException ex){
        ...
    }
}
  • p1不捕获异常,但是要用throws语句声明它可能会抛出IOException。throws是Java关键字。
void p1() throws IOException{
    ...
    p2();
    ...
}

无论采用哪种方案,程序员都会(强迫)了解到p2会抛出异常这件事情。Java通过这种方式,强制程序员来处理异常。

我们假设p2本身是可能抛出IOException异常的,它的定义是什么样子的呢?同样的,它需要用throws来声明会抛出异常,而真正的异常对象是通过throw语句来抛出。

void p2() throws IOException{
    ...
    if(...){  // 异常情况出现,抛出异常对象
       throw new IOException("file read exception");
    }
}

注意区别throw和throws关键字的区别。throw用来抛出异常对象,throws用来声明函数可能会抛出某个异常。如果方法会抛出多个异常,那么方法声明的throws语句也需要声明多个异常类型,各类型之间用逗号’,’隔开。比如

void p2() throws IOException, AnotherExceptionType{
    ...
}

异常处理小结

可以说编码中应该关注的真正的”异常“应该是Exception类及其子类(排除RuntimeException及其子类)的检查异常。程序中必须处理它们,或者是用try-catch捕获它们,或者是声明方法会抛出此类异常,将处理的任务交给调用方法。

异常类型归类处理方式
Exception类及其子类(排除RuntimeException及其子类)检查异常可能会出现的异常,一定要处理
RuntimeException及其子类非检查异常可以避免的异常,不需要处理,编码中注意避免它们的出现
Error及其子类非检查异常不是代码问题,不需要处理

异常处理要注意的情况

注意,为了方便起见,示例代码都是处理非检查异常,实际编码中是不需要处理它们的,而是需要处理检查异常的。

多个子类异常可以用父类异常统一捕获

因为父类对象变量可以引用子类对象,因此如果try块中可能会出现多种异常,但是它们有共同的父类,那可以只用一个父类的catch来捕获它们。

// Use parent exception to catch child exception.
class ExcDemo4 {
    public static void main(String args[]) {
        // Here, numer is longer than denom.
        int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };
        for(int i=0; i<numer.length; i++) {
            try {
                System.out.println(numer[i] + " / " +
                denom[i] + " is " +
                numer[i]/denom[i]);
            }
            catch (RuntimeException exc) {
                // catch the exception
                System.out.println("run time exception occour");
            }
        }
    }
}

如果catch异常类型有父子关系,那么要将子类异常的catch放在父类的前面

这里要注意一点,如果catch中捕获的异常类型具有父子关系,那么在catch中要注意它们的顺序,子类异常捕获catch语句要放在父类之前。

这是因为将try抛出的异常对象与各个catch异常类型相匹配时是从上往下顺序匹配的。而子类异常对象的类型是可以视为父类异常类型的。如果捕获父类异常类型的catch放在前面,子类异常对象就会被它捕获,这样的话真正捕获这个子类异常类型的catch永远得不到被执行的机会。

// Use parent exception to catch child exception.
class ExcDemo5_Wrong {
    public static void main(String args[]) {
        // Here, numer is longer than denom.
        int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };
        for(int i=0; i<numer.length; i++) {
            try {
                System.out.println(numer[i] + " / " +
                denom[i] + " is " +
                numer[i]/denom[i]);
            }
            catch (RuntimeException exc) {  // 父类异常在前,错误的写法
                // catch the exception
                System.out.println("run time exception occour");
            }
            catch (ArithmeticException exc) {  
                // catch the exception
                System.out.println("Can't divide by Zero!");
            }           
        }
    }
}

上面代码中第二个catch就是无法被运行到的代码,因为RuntimeException是ArithmeticException的父类,try抛出的ArithmeticException异常对象会先被第一个catch捕获,这样放在后面的真正捕获ArithmeticException的catch就无法被执行。

必须要这样安排catch的顺序

// Use parent exception to catch child exception.
class ExcDemo5_Right {
    public static void main(String args[]) {
        // Here, numer is longer than denom.
        int numer[] = { 4, 8, 16, 32, 64, 128, 256, 512 };
        int denom[] = { 2, 0, 4, 4, 0, 8 };
        for(int i=0; i<numer.length; i++) {
            try {
                System.out.println(numer[i] + " / " +
                denom[i] + " is " +
                numer[i]/denom[i]);
            }           
            catch (ArithmeticException exc) {  // 子类异常在前,正确的写法
                // catch the exception
                System.out.println("Can't divide by Zero!");
            }   
            catch (RuntimeException exc) {
                // catch the exception
                System.out.println("run time exception occour");
            }       
        }
    }
}

可以理解成,异常类型越具体就越要放在catch列表中的前面。

finally

为了在离开try-catch时执行一些语句,可以将这些语句放到finally块中,不论是否发生异常,finally语句块中的代码都会执行。它是try-catch-finally模块最后执行的语句。

class ExcDemo6 {
    public static void main(String args[]) {
        int nums[] = new int[4];
        try {
            System.out.println("Before exception is generated.");
            // Generate an index out-of-bounds exception.
            nums[7] = 10;
            System.out.println("this won't be displayed");
        }
        catch (ArrayIndexOutOfBoundsException exc) {
            // catch the exception
            System.out.println("Index out-of-bounds!");
        }finally{
            System.out.println("Leaving try");
        }
        System.out.println("After catch statement.");
    }
}

程序运行结果显示:
Before exception is generated.
Index out-of-bounds!
Leaving try
After catch statement.

如果异常没有发生,

class ExcDemo6_2 {
    public static void main(String args[]) {
        int nums[] = new int[4];
        try {
            System.out.println("Before exception is generated.");
            // not Generate an index out-of-bounds exception.
            nums[2] = 10;
            System.out.println("this will be displayed");
        }
        catch (ArrayIndexOutOfBoundsException exc) {
            // catch the exception
            System.out.println("Index out-of-bounds!");
        }finally{
            System.out.println("Leaving try");
        }
        System.out.println("After catch statement.");
    }
}

程序运行结果:
Before exception is generated.
this will be displayed
Leaving try
After catch statement.

不论异常是否发生,finally里面的代码都会被运行。

关于检查异常的一个例子

下面代码用来复制文件,其中对文件的创建和读写均可能会产生检查异常IOException,

```java
public class CheckedExcDemo1{
    public static void main(String args[]){
        File source = new File("from.txt");
        File dest = new File("to.txt");
        copyFileUsingFileStreams(source, dest);
    }
    private static void copyFileUsingFileStreams(File source, File dest) {    
        InputStream input = null;    
        OutputStream output = null;    
        try {
           input = new FileInputStream(source);
           output = new FileOutputStream(dest);        
           byte[] buf = new byte[1024];        
           int bytesRead;        
           while ((bytesRead = input.read(buf)) > 0) {
               output.write(buf, 0, bytesRead);
           }
           System.out.println("copy done!");
        }catch(IOException e) {
            System.out.println("Error open or read/write file");            
        } finally {
            try {
                if (input != null) {
                    input.close();
                }
                if (output != null) {
                    output.close();
                }
            }catch(IOException exc) {
                System.out.println("Error Closing File");
            }
        }
}
```

除了在copyFileUsingFileStreams()方法中处理IOException,还可以不处理,但是需要在方法头声明方法会抛出异常。

```java
import java.io.*;

public class CheckedExcDemo2{
    public static void main(String args[]) {
        File source = new File("from.txt");
        File dest = new File("to.txt");
        try {
            copyFileUsingFileStreams(source, dest);
        }catch(IOException exc) {
            System.out.println("Error open or read/write or close file");
        }
    }

    private static void copyFileUsingFileStreams(File source, File dest)
            throws IOException {    
        InputStream input = null;    
        OutputStream output = null;    
        try {
               input = new FileInputStream(source);
               output = new FileOutputStream(dest);        
               byte[] buf = new byte[1024];        
               int bytesRead;        
               while ((bytesRead = input.read(buf)) > 0) {
                   output.write(buf, 0, bytesRead);
               }
               System.out.println("copy done!");
        } finally {
            if (input != null) {
                input.close();
            }
            if (output != null) {
                output.close();
            }                   
        }
    }
}
```

高级话题

rethrowing异常

chained异常

自定义异常

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值