异常处理

文章目录

    • 什么是异常处理
    • Java中对异常的分类
    • 处理异常
    • 处理多个异常
    • 注意事项
    • throws和throw
    • 自定义异常

什么是异常处理

先想想,什么是正常?一个程序从头跑到尾,所有的步骤都是按照我们的预期运行的,这就是正常
异常,自然就是和正常不同了啊。简单来说,异常就是程序运行过程中出现了问题,或者出现了和我们预期不同的情况。
因此,我们可以认为,异常就是程序运行过程中出现的错误。
这种情况很多,例如我们设计了一个计算器,用户用它来计算“10/0”;或者我们设计了一个读取并分析文件的程序,用户却输错了文件名,这就要求程序打开一个不存在的文件;或者我们需要通过网络发送文件此时网络却断开了,我们需要写入日志信息磁盘却没有可用空间了……
可以说,异常情况非常多,要写出一个错误少、运行稳定的程序,就需要考虑到各种异常情况并加以处理。在以前,这是非常考验开发人员的经验和细心程度的。
Java为了提高开发效率,设计了异常处理机制。在Java的异常处理机制中,所有的异常情况都被封装到对象中。当异常发生时,JVM就会生成对应的异常对象——一般称之为“抛出异常对象”,或者“抛出异常”,同时暂停程序的执行。如果我们为这种情况编写了合适的代码——一般称为“异常处理”——这些异常对象就会被捕获并进行相应的处理,处理完毕后,继续执行程序。
这个机制有点像应急预案:我们先针对某些问题设计一个处理方案,异常对象类似于一种信号,当这个信号出现时,就意味着某个问题发生了,然后就可以启动相应的预案,待处理完毕后继续后面的操作。
看到这里,可能有人会问:如果我们没有预料到某个问题,也就没有制定对应的处理方案,怎么办?这个时候,JVM会把这个异常向上级代码(比如某段代码发生了异常,这段代码位于方法A中,方法A被方法B调用,方法A就是这段代码的上级,方法B则是方法A的上级)逐级上报,如果上级代码中有相应的处理方案,就进行处理;如果没有任何处理方案,那么程序到此暂停执行,并把异常信息交给用户,由用户进行下一步处理。

Java中对异常的分类

Java中,表示异常的类都实现了Throwable接口,或者说,所有的异常类都是Throwable接口的子类。Throwable接口有两个直接子类,一是Error类,二是Exception类。Error,翻译为“错误”,代表那些程序自身无法处理的问题,例如资源耗尽、JVM崩溃等;Exception,翻译为“异常”,代表那些不太严重、程序自身可以处理的问题,例如除0操作、文件名错误等。Java中的异常处理机制针对的就是Exception对象代表的各类问题。
Exception类的子类分为两大类,一是RuntimException类及其子类,这类异常只有在运行时才能确定是否会发生,即使不处理编译也能通过,称为运行时异常;二是其它子类,这些异常如果不被处理,编译时会报错,称为编译时异常。
常见的运行时异常包括:ArithmeticException(算数异常),IndexOutOfBoundsException(下标越界异常),ClassCastException(类型转换异常),NullPointException(空指针异常),NumberFormatException(数字格式化异常)等等。

处理异常

在Java中处理异常也很简单,就是一个套路:

try{
    可能产生异常的代码;
}catch(异常对象){
    发生异常时的处理代码;
}finally{
    无论是否发生异常都会执行的代码;
}

try语句块中包含可能会发生异常的代码,当异常发生时,JVM会暂停程序执行,将异常封装为对象,该异常对象会被catch语句所捕获,catch语句会将捕获到的异常对象和括号中的异常对象的类型进行比对,如果实际发生的异常属于catch括号中的异常类型或是其子类型,就执行catch语句块中的处理代码;如果写了多个catch语句,就会从上到下依次比对,然后执行第一个匹配的类型,如果没有相匹配的类型,则将异常抛出。
finally部分是可选的,无论异常是否发生,其中的代码都会执行。
为了便于理解,来看一个例子:

import java.util.Scanner;
public class ExceptionDemo01 {
    public static void main(String[] args) {
        int a,b;
        Scanner sc=new Scanner(System.in);
        System.out.println("请输入被除数:");
        a=sc.nextInt();
        System.out.println("请输入除数:");
        b=sc.nextInt();
        try{
            System.out.println("a/b="+(a/b));
        }catch (ArithmeticException e){
            System.out.println("发生了除以0的情况,除数不能为0");
            //输出异常信息
            //e.printStackTrace();
        }
    }
}

在这个程序中,用户先输入被除数和除数,然后让两者相除,如果发生异常,JVM会将其封装成一个ArithmeticException类型的对象,当catch语句捕捉到这个异常对象时,会和括号中的异常对象比对,如果两者属于同一个类,或抛出的异常对象属于括号中异常类的子类,那么就可以通过对象e来引用这个抛出的异常对象了(回忆一下多态的内容)。之后就可以在catch语句块中进行相对应的处理了。
这个程序中提供了两种处理方式,一是提醒输入错误,二是注释部分,显示异常的相关信息。
但这两种方法都没有什么用处,因为不管是提示输入错误,还是显示异常信息,程序都结束了,用户也没有机会改正——从结果的角度看,这种方式和不进行异常处理区别不大。
所以,我们应该让用户改错的机会,从而保证程序的功能最终可以实现。
我们来看看改进版的程序:

import java.util.Scanner;

public class ExceptionDemo02 {
    public static void main(String[] args) {
        int a, b;
        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请输入被除数:");
            a = sc.nextInt();
            System.out.println("请输入除数:");
            b = sc.nextInt();
            try {
                System.out.println("a/b=" + (a / b));
                //如果不发生异常,就会中止循环
                break;
            } catch (ArithmeticException e) {
                System.out.println("发生了除以0的情况,请重新输入");
            } finally {
                System.out.println("----------END----------");
            }
        }
    }
}

在这个程序中,我们把相关代码放到while循环中,而且循环条件是true,这就意味着程序会一直执行——除非没有发生异常,会执行break;语句,中止循环。
而且,我们增加了finally语句部分。在执行时我们会发现,无论异常是否会发生,finally语句中的代码都会执行。
这里留给大家一个问题:如果用户输入错误,程序也会输出----------END----------,然后让用户重新输入数据,这和我们的使用习惯不相符,请大家思考一下,如何才能保证发生异常时不会输出----------END----------,不发生异常时才会输出呢?

处理多个异常

可能已经有人发现了,刚才的例子其实还有一个缺陷,如果用户输入的除数不是0而是或者zero怎么办?
在这个例子中,用户输入的数据其实是字符串,然后转换为了数字。字符串"0"转换成数字0倒也自然,“零”怎么转换呢?除非我们不用标准库,而是另外写程序……太麻烦了,而且要考虑的问题太多了,最重要的是,只要用户稍微注意一点,就可以避免这些问题。所以我们还是让用户输入阿拉伯数字吧,这时只要防止用户有意无意输入非阿拉伯数字形式的信息即可。
如果用户输入信息的形式错了,就会产生InputMismatchException类型的异常,我们需要对它进行捕捉和处理。刚才例子可以修改为:

import java.util.InputMismatchException;
import java.util.Scanner;
public class ExceptionDemo03 {
    public static void main(String[] args) {
        int a, b;
        Scanner sc = new Scanner(System.in);
        while (true) {
            try {
                System.out.println("请输入被除数:");
                a = sc.nextInt();
                System.out.println("请输入除数:");
                b = sc.nextInt();
                System.out.println("a/b=" + (a / b));
                //如果不发生异常,就会中止循环
                break;
            } catch (ArithmeticException e) {
                System.out.println("发生了除以0的情况,请重新输入");
            }catch (InputMismatchException e){
                //将错误形式的信息清理出缓冲区
                sc.nextLine();
                System.out.println("您输入的数字格式有误,请重新输入");
            }finally {
                System.out.println("----------END----------");
            }
        }
    }
}

在这个程序中,我们把负责用户输入信息的代码也放倒了try语句块中,这样就可以处理输入时产生的异常了。此时try语句块中的代码可能会产生两种异常,ArithmeticExceptionInputMismatchException类型,我们就在try的后面针对两种异常写两个catch语句即可。在处理InputMismatchException类型异常时,我们加了一行sc.nextLine();,这是由于:用户输入的数据会先保存在缓冲区中,然后由程序取出数据并进行处理;如果用户输错了数据,由于程序无法处理,那么原来的错误数据就会留在缓冲区中,到下一次循环时,程序读取到的还是错误的数据,这就阻止我们继续输入,所以要使用这一行代码把错误数据读出缓冲区。
还记得我们之前做过一个猜数字的游戏吗?当时的程序没有考虑用户会输入非数字的数据,但在现实中这种问题时有发生,大家可以思考一下, 如何修改当时的程序,使我们的猜数字游戏能够避免用户输入错误格式的数据呢?

注意事项

需要注意,发生异常时,JVM会从上至下依次检查,看看哪个catch语句能处理当前的异常,如果找到了,那就直接使用,而不再考虑后面有没有更合适的。假设现在有个程序,从上到下写了三个catch语句,分别处理A,B和C三种异常,其中A是B的父类,B是C的父类。如果发生了A类异常,毫无疑问是由第一个catch语句来处理,那如果发生了B类或者C类异常呢?也是第一个catch语句来处理,因为A是B和C的父类啊。
就好像,你是一个马戏团老板,你跟你的驯兽师说:如果动物逃跑了,一枪崩了就是;如果是猫跑了,你拿出小鱼干引诱一下就回来了;如果是狗跑了,你扔块肉出去它就回来了。之后狗跑了,驯兽师拿出你的“应急预案”,看到第一句:“如果动物逃跑了,一枪崩了就是”,然后掏出枪把狗打死了……
这就是Java的异常处理机制,只要能找到一个处理方案,绝对不会去看后面有没有更好的处理方案。所以,在进行异常处理时,越是具体的异常处理,越是要放到前面,而那些具体异常类的父类,尽量放到后面,辈分越高越往后面放。

  • 鉴于Java识别异常类型的机制,千万不要写catch(Exception e)这种异常处理代码,不然此后所有的catch语句都会失效,除非这是最后一个catch语句。
  • try语句块中尽量不要包含那些不会发生异常或者通过其他方法已经处理异常的代码。try中的代码太多,可能会降低性能,而且更容易在catch语句上出问题。
  • finally语句一般用于释放被打开的资源,这样就可以保证程序无论是否出现异常,资源都可以被正常释放,减少资源占用。

04

throws和throw

这两个词看起来很像,含义也差不多:都是“抛出”的意思。不过应用场合不同。
throws用于方法,允许方法不处理那些必须处理的编译时异常,而将这些异常交给调用该方法的代码处理。使用方法是:方法返回值 方法名(参数列表) throws 异常类1,异常类2...{方法体}
throw用于手工抛出一个指定类型的异常——即使此时并没有真的异常发生也可以抛出。使用方法是:throw new 异常类构造方法;
例如:

import java.util.InputMismatchException;
import java.util.Scanner;

public class ExceptionDemo04 {
    public static double divide(double a,double b){
        if(b==0){
            //如果除数为0,抛出ArithmeticException类型的异常
            throw new ArithmeticException();
        }
        return a/b;
    }
    //本方法在运行时,可能会出现ArithmeticException, InputMismatchException两种异常,本方法不予处理,抛出
    public static double inputDevide() throws ArithmeticException, InputMismatchException {
        double a,b;
        Scanner sc=new Scanner(System.in);
        System.out.println("请输入被除数:");
        a = sc.nextDouble();
        System.out.println("请输入除数:");
        b = sc.nextDouble();
        return divide(a,b);
    }

    public static void main(String[] args) {
        double result=inputDevide();
        System.out.println(result);
    }
}

我们在divide()方法中,抛出了一个ArithmeticException异常——这是强制抛出的,因为实数相除时,除数为0,结果为Infinity,无限大,所以这里时强制抛出的。在inputDevide()方法中,可能会产生InputMismatchException异常,但不想在这个方法中处理,于是使用throws关键字将其抛出,由于这个方法中也调用了divide()方法,可能产生ArithmeticException异常,也可以一同抛出。抛出多个异常时,这些异常类用逗号分隔开。如果这些异常类具有相同的父类,也可以只抛出其父类,例如本例中inputDevide()方法可以这样抛出异常:throws Exception
在主方法中,调用了inputDevide()方法,就需要在主方法中处理ArithmeticExceptionInputMismatchException异常。不过由于这两个异常都是运行时异常,所以Java编译器并没有强制要求处理,如果这些异常中包含编译时异常,编译器就会强制要求处理异常。

自定义异常

Java的标准库中提供了大量异常类,可以满足我们的大部分需要,但有时也需要我们自定义异常类。自定义异常的一般方法是:

  1. 定义异常类,其父类为Exception类;
  2. 在构造方法中使用super关键字调用父类构造方法;
  3. 在合适的时机使用throw关键字抛出异常。

我们来看一个例子:

//自定义异常类
class DivideByZeroException extends Exception{
    public DivideByZeroException(){
        super();
    }
    //如果希望提供异常信息,可以设计带有异常信息的构造方法
    public DivideByZeroException(String message) {
        super(message);
    }
}
public class ExceptionDemo05 {
    public static double divide(double a,double b) throws DivideByZeroException{
        if(b==0){
            //DivideByZeroException是编译时异常,必须进行处理,这里通过throws抛出
            throw new DivideByZeroException("除数为0,不能正常计算!");
        }
        return a/b;
    }
    public static void main(String[] args) {
        try {
            System.out.println(divide(25.6,0));
        } catch (DivideByZeroException e) {
            //输出异常信息
            System.out.println("异常信息是:"+e.getMessage());
        }
    }
}

由于DivideByZeroException类继承自Exception类,它是一个编译时异常,所以在出现这个异常时,要么使用try…catch语句进行异常处理,要么在所属方法中使用throws抛出。但必须要在某段代码中对其进行处理,否则最终会抛给用户——也就是程序会在异常发生时退出。

04

你可能感兴趣的:(Java11学习,Java,异常处理,自定义异常)