一篇文章带你理解并掌握Java的异常

2022-07-25,,,,

文章目录

  • 前言
  • 一、异常处理
  • 二、异常类型
  • 三、异常处理的其它方面
    • 1.声明异常
    • 2.抛出异常
    • 3.捕获异常
    • 4.示例学习:声明、抛出和捕获异常
  • 四、finally子句
  • 五、何时使用异常
  • 六、重新抛出异常
  • 七、链式异常
  • 八、创建自定义异常类

前言

异常是运行时错误,异常处理使得程序可以处理运行时错误,并且继续通常的执行。异常机制提供了程序退出时的安全通道。当出现错误后,程序执行流程发生改变,程序的控制权转移到异常处理器。

编译错误:程序没有遵循语法规则,编译程序能够自己发现并提示我们错误的原因和位置。

运行时错误:程序在执行时,运行环境发现了不能执行的操作。

逻辑错误:程序没有按照预期的逻辑顺序执行。

在程序运行过程中,如果JVM检测出一个不可能执行的操作,就会出现运行时错误。例如,如果使用一个越界的下标访问数组,程序就会产生一个ArrayIndexOutOfBoundsException的运行时错误;如果程序需要输入一个整数的时候用户输入了一个double值,会得到InputMismatchException的运行时错误。

在Java中,运行时错误会被作为异常抛出。异常就是一种对象,表示阻止正常运行的错误或者情况。如果异常没有处理,那么程序将会非正常终止。


一、异常处理

异常是从方法抛出的,方法的调用者可以捕获并处理该异常。

为了演示异常处理,包括异常是如何创建以及抛出的,我们从读取两个整数并显示它们商的例子开始。

import java.util.Scanner;

/**
 * @author MNH
 * @version 1.0
 * @project Name: JAVA基础
 * @file Name: Quotient
 * @desc 功能描述
 * @by IDE: IntelliJ IDEA
 */
public class Quotient {
    public static void main(String[] args) {
        Scanner input=new Scanner(System.in);
        System.out.println("Enter two integer:");
        int num1=input.nextInt();
        int num2=input.nextInt();
        System.out.println(num1+"/"+num2+"="+(num1/num2));
    }
}

如果输入0赋值给第二个数字,那就会产生一个运行时错误,因为不能用一个整数除以0(注意,一个浮点数除以0是不会产生异常的)。解决这个错误的一个简单方法就是添加一个if语句来测试第二个数字。

import java.util.Scanner;

/**
 * @author MNH
 * @version 1.0
 * @project Name: JAVA基础
 * @file Name: QuotientWithIf
 * @desc 功能描述
 * @by IDE: IntelliJ IDEA
 */
public class QuotientWithIf {
    public static void main(String[] args) {
        Scanner input=new Scanner(System.in);
        System.out.println("请输入两个整数:");

        int num1=input.nextInt();
        int num2=input.nextInt();

        if(num2!=0){
            System.out.println(num1+"+"+num2+"="+(num1/num2));
        }else{
            System.out.println("被除数不能为0");
        }
    }
}

为了介绍异常处理,我们使用一个方法计算商,程序如下:

import java.util.Scanner;

/**
 * @author MNH
 * @version 1.0
 * @project Name: JAVA基础
 * @file Name: QuotientWithMethod
 * @desc 功能描述
 * @by IDE: IntelliJ IDEA
 */
public class QuotientWithMethod {
    public static int quotient(int num1,int num2){
        if(num2==0){
            System.out.println("被除数不能为0");
            System.exit(1);
        }
        return num1/num2;
    }

    public static void main(String[] args) {
        Scanner input=new Scanner(System.in);

        System.out.println("请输入两个整数:");

        int num1=input.nextInt();
        int num2=input.nextInt();

        System.out.println(num1+"/"+num2+"="+quotient(num1,num2));
    }
}

方法quotient返回两个整数的商,如果num2为0,则不能返回一个值,程序在System.exit(1)处终止,这显然是一个问题。不应该让方法来终止程序—应该由调用者决定是否终止程序。

方法如何通知它的调用者一个异常产生了呢?Java可以让一个方法抛出一个异常,该异常可以被调用者捕获和处理。

import java.util.Scanner;

/**
 * @author MNH
 * @version 1.0
 * @project Name: JAVA基础
 * @file Name: QuotientWithException
 * @desc 功能描述
 * @by IDE: IntelliJ IDEA
 */
public class QuotientWithException {
    public static void quotient(int num1, int num2) {
        try {
            if (num2 == 0) {
                throw new ArithmeticException("被除数不能等于0");//抛出一个异常
            }
            System.out.println(num1 + "/" + num2 + "=" +(num1/num2));
        }catch (ArithmeticException e) {
            System.out.println("被除数不能为0");
        }
        System.out.println("程序继续......");
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("请输入两个整数:");

        int num1 = input.nextInt();
        int num2 = input.nextInt();

        quotient(num1,num2);
    }
}

//异常就是从一个异常类创建的对象

//本程序中异常就是Java.lang.ArithmeticException的构造方法ArithmeticException(str)被调用以创建异常类对象,str是描述异常的消息

如果num2等于0,,方法通过执行以下语句抛出一个异常:throw new ArithmeticException(“被除数不能为0”)。在这种情况下,抛出的值为new ArithmeticException(“被除数不能为0”),称为一个异常。throw语句的执行称为抛出一个异常。异常就是从异常类创建的对象。在这种情况下,异常类就是java.lang.ArithmeticException。构造方法ArithmeticException(str)被调用以构建一个异常对象,其中str是描述异常的消息。

当异常被抛出时,正常的执行流程被中断。就像他的名字所提示的,“抛出异常”就是将异常从一个地方传递到另一个地方。调用方法的语句也包含在一个try块和一个catch块中。try块包含了正常情况下执行的代码,异常被catch块所捕获。catch块中的代码被执行以处理异常,之后,catch块之后的语句被执行。

throw语句类似方法的调用,但不同于调用方法的是,它调用的是catch块。从某种意义上讲,catch块就像带参数的方法定义,这些参数匹配抛出的值的类型。但是它不像方法,在执行完catch块之后,程序控制不返回到throw语句,而是执行catch块后的下一条语句。

catch(ArithmeticException ex)中的标识符ex的作用很像是方法中的参数,这个参数称为catch块中的参数。ex之前的类型(如,ArithmeticException)指定了catch块可以捕获的异常类型。一旦捕获该类型,就能从catch块体中的参数访问这个抛出的值。

一个异常可能是通过try块中的throw语句直接抛出,或者调用一个可能会抛出异常的方法而抛出。

异常处理的优点:它能使方法抛出一个异常给它的调用者,并由调用者处理该异常。如果没有这个能力,那么被调用的方法就必须自己处理异常或者终止该程序。被调用的方法通常不知道在出现错误时应该做些什么,只有调用者自己清楚出现错误时需要做些什么。异常处理最根本的优势就是将检测错误(由被调用的方法完成)从处理错误中分离出来。

import java.util.Scanner;

/**
 * @author MNH
 * @version 1.0
 * @project Name: JAVA基础
 * @file Name: InputMismatchException
 * @desc 功能描述
 * @by IDE: IntelliJ IDEA
 */
public class InputMismatchException {
    public static void main(String[] args) {
        Scanner input=new Scanner(System.in);
        boolean continueInput=true;

        while(continueInput){
            try{
                System.out.println("请输入一个整数:");
                input.nextInt();
                continueInput=false;
            }catch(java.util.InputMismatchException e){
                System.out.println("输入的不是整数,请重新输入");
                input.nextLine();
            }
        }
    }
}

//变量continueInput控制循环,它的初始值是true,当键入一个整数时,该值就变成false

当执行input.nextInt()时,如果输入的不是一个整数,就会产生一个InputMismatchException异常。

二、异常类型

异常是对象,对象都采用类来定义。异常的根类是java.lang.Throwable。Throwable类是所有异常类的根,所有Java异常类都直接或间接地继承自Throwable,可以通过继承Exception或者Exception的子类来创建自己的异常类。

这些异常类可以分为三种主要类型:系统错误、异常或运行时异常。(1)系统错误:系统错误是由Java虚拟机抛出的,用Error类表示的。Error类描述的是内部系统错误,这样的错误很少发生。如果发生,除了通知用户以及尽量稳妥地终止程序外,几乎什么也做不了。(2)异常:异常是用Exception类表示的,它描述的是由你的程序和外部环境引起的错误,这些错误能被程序捕获和处理。(3)运行时异常:运行时异常是用RuntimeException类表示的,它描述的是程序设计错误,例如,错误的类型转换、访问一个越界数组或数值错误,运行时异常通常表明了编程错误。

RuntimeException、Error以及它们的子类都称为免检异常,在大多数情况下,免检异常都会反映出程序设计上不可恢复的逻辑错误。例如,如果通过一个引用变量访问一个对象之前并未将一个对象赋值给它,就会抛出NullPointerException异常;如果访问一个数组的越界元素,就会抛出IndexOutOfBoundsException异常。这些都是程序中必须纠正的逻辑错误。免检异常可能在程序的任何一个地方出现,为避免过多地使用try-catch块,Java语言不强制要求编写代码捕获或声明免检异常。所有其它异常称为必检异常,意味着编译器会强制程序员检查并通过try-catch块处理它们,或者在方法头中声明。

必检异常增加了代码的健壮性,但是为了对异常进行抛出、捕获和处理,需要增加较多代码,会降低代码的可读性。如果异常影响到系运行的安全性和正确性的时候,必须对必检异常进行处理,否则这些必检异常是可以转换成免检异常。

三、异常处理的其它方面

Java的异常处理模型基于三种基本操作:声明一个异常、抛出一个异常、捕获一个异常。

1.声明异常

Java解释器调用main方法开始执行一个程序,当前执行的语句必属于某个方法,每个方法都必须声明它可能抛出的必检异常的类型,称为声明异常。因为任何代码都可能发生系统错误和运行时错误,因此Java不要求在方法中显示地声明Error和RuntimeException。然而,方法要跑出的其它异常都必须在方法头中显示声明,这样,方法的调用者会被告知有异常。

为了在方法中声明一个异常,就要在方法头中使用关键字throws,如:public void method() throws IOException,关键字throws表明method()方法可能会抛出异常IOException。如果方法可能抛出多个异常,就可以在关键字throws后添加一个用逗号分隔的异常列表:public void method() throws Exception1,Exception2,…,ExceptionN。如果父类中的方法没有声明异常,那么就不能在子类中对其重写时声明异常。

2.抛出异常

检测到错误的程序可以创建一个合适的异常类型的实例并抛出它,这称为抛出一个异常,如throw new IllegalArgumentException(“Wrong argument”)。IllegalArgumentException是Java API中的一个异常类,通常,Java API中每个异常类都至少有两个构造方法:一个无参构造方法和一个带有可以描述这个异常的String参数的构造方法,该参数称为异常消息,它可以通过一个异常对象调用getMessage()获取。

声明异常的关键字是throws,抛出异常的关键字是throw。

3.捕获异常

当抛出一个异常时,可以在try-catch块中捕获和处理它。

try{

}catch(Exception1 ex1){

}catch(Exception2 ex2){

}
...
catch(ExceptionN exN){

}

如果在执行try块的过程中没有出现异常,则跳过catch子句。如果try块中的某条语句抛出一个异常,Java就会跳过try块中剩余的语句,然后开始查找处理这个异常的代码,处理异常的这个代码称为异常处理器。从第一个到最后一个逐个检查catch块,判断在catch块中的异常类实例是否是该异常对象的类型。如果是,就将该异常对象赋值给所声明的变量,然后执行catch块中的代码。如果没有发现异常处理器,Java会退出这个方法,把异常传递给这个方法的调用者,继续同样的过程来查找处理器。如果在调用的方法链中找不到处理器,程序就会终止并且在控制台上打印出错误信息。查找处理器的过程称为捕获一个异常。

假设main方法调用method1()方法,method1()方法调用method2()方法,method2()方法调用method3()方法,method3()方法抛出一个异常,考虑下面的情形:

main method{
	try{
		method1();
		statement1
	}catch(Exception1 ex1){

	}
	statement2
}

class method1{
	try{
		method2();
		statement3
	}catch(Exception2 ex2){

	}
	statement4
}

class method2{
	try{
		method3();//一个异常在method3()方法中抛出
		statement5
	}catch(Exception3 ex3){

	}
	statement6
}

如果异常类型是Exception3,它就会被method2()中处理异常ex3的catch块捕获,跳过statement5,然后执行statement6。

如果异常类型是Exception2,,则退出method2()方法,控制被返回给method1()方法,而这个异常就会被method1()中处理异常ex2的catch块捕获,跳过statement3,然后执行statement4。

如果异常类型是Exception1,则退出method1()方法,控制被返回给main()方法,而这个异常就会被main()方法中处理异常ex1的catch块捕获,跳过statement1,然后执行statement2。

如果异常类型没有在method2、method1、main方法中捕获,程序就会终止,不执行statement1和statement2。

各种异常类都可以从一个共同的父类中派生。如果一个catch块可以捕获一个父类的异常对象,那么它就可以捕获那个父类的所有子类的异常对象。

在catch块中异常被指定的顺序是非常重要的,如果父类的catch块出现在子类的catch块之前,就会导致编译错误。

try{

}catch(Exception e){

}catch(RuntimeException ex){

}

//错误的顺序
try{

}catch(RuntimeException e){

}catch(Exception ex){

}

//正确的顺序

对于使用同样的代码处理多种异常的情况,可以使用JDK7的新的多捕获特征简化异常的代码书写。语法是:

catch(Exception1 | Exception2 | ... |ExceptionN e){

}

每个异常使用竖线(|)与下一个分隔,如果其中一个异常被捕获,则执行处理的代码。

4.示例学习:声明、抛出和捕获异常

创建一个CircleWithException类的对象,如果setRadius(double radius)中radius是负数,则抛出一个IllegalArgumentException异常。

/**
 * @author MNH
 * @version 1.0
 * @project Name: JAVA基础
 * @file Name: CircleWithException
 * @desc 功能描述
 * @by IDE: IntelliJ IDEA
 */
public class CircleWithException {
    private double radius;
    private static int numberOfCircle=0;

    public CircleWithException(double radius){
        setRadius(radius);
        numberOfCircle++;
    }

    public CircleWithException(){
        this(0);
    }

    public double getRadius(){
        return radius;
    }

    public void setRadius(double radius){
        if(radius>=0) {
            this.radius = radius;
        }else{
            throw new IllegalArgumentException("半径不能为负数");
        }
    }

    public double area(){
        return radius*radius*3.14;
    }

    public static int getNumberOfCircle(){
        return numberOfCircle;
    }

    public static void main(String[] args) {
        try{
            CircleWithException circleWithException1=new CircleWithException(2.0);
            CircleWithException circleWithException2=new CircleWithException(-2.0);
            CircleWithException circleWithException3=new CircleWithException();
        }catch(IllegalArgumentException e){
            System.out.println(e.toString());
        }

        System.out.println("共有"+numberOfCircle+"个圆被创建");
    }
}

IllegalArgumentException是异常类RuntimeException(免检异常)的子类,所以,如果不使用try语句,这个测试程序也能编译成功。

万一出现了异常,程序仍然会继续,而如果处理器没有捕获到这个异常,程序就会突然中断。

当没有异常发生时,try-catch的存在对系统性能的影响很小,可以说不会引起额外的系统开销。

异常发生后:(1)初始化异常对象(2)从调用栈返回(3)沿方法调用链来传播异常,以找到它的异常处理器。

四、finally子句

无论异常是否发生,finally子句总会被执行。

有时候,不论异常是否出现或是否被捕获,都希望执行某些代码。Java的finally子句可以用来实现这个目的,finally子句的语法如下:

try{
	statements;
}catch(Exception e){

}finally{
	finalStatements;
}

在任何情况下,finally块中的代码都会执行,不论try块中是否出现异常或者是否被捕获。考虑以下三种可能出现的情况:(1)如果try块中没有出现异常,执行fianlStatements,然后执行try语句的下一条语句;(2)如果try块中有一条语句引起了异常并被catch块捕获,会跳过try块的其它语句,执行catch块和finally子句,并执行try语句后的下一条语句;(3)如果try块中的一条语句引起异常,但是没有被任何catch块捕获,就会跳过try块中的其它语句,执行finally子句,并且将异常传递给这个方法的调用者。

即使在到达finally块之前有一个return语句,finally块还是会执行,同时使用finally子句时可以省略catch块。

五、何时使用异常

当错误需要被方法的调用者处理的时候,方法应该抛出一个异常。

try块包含正常情况下执行的代码,catch块包含异常情况下执行的代码。异常处理将错误处理代码从正常的编程中分离出来,这样,就可以使程序更易读、更易修改。由于异常处理需要初始化新的异常对象,需要从调用栈返回,而且还需要沿着方法调用链来传播异常以便找到它的异常处理器,所以异常处理需要更多的时间和资源。

异常发生在方法中,如果想让该方法的调用者处理异常,应该创建一个异常对象并将其抛出。如果能在发生异常的方法中处理异常,那么就不需要抛出或使用异常。一般来说,一个项目中多个类都会发生的共同异常应该考虑设计为一个异常类。对于发生在个别方法中的简单错误最好进行局部处理,无需抛出异常,这可以通过使用if语句来检测错误并实现。

六、重新抛出异常

如果异常处理器不能处理一个异常,或者只是简单地希望他的调用者注意到该异常,Java允许该异常处理器重新抛出异常。

try{
	statements;
}catch(Exception ex){
	throw ex;
}

语句throw ex重新抛出异常给调用者,以便调用者的其它处理器获得处理异常ex的机会。

七、链式异常

与另一个异常一起抛出一个异常,构成了链式异常。

有时候,可能需要同最初异常一起抛出一个新异常(带有附加信息),称为链式异常。

/**
 * @author MNH
 * @version 1.0
 * @project Name: JAVA基础
 * @file Name: ChainedExceptionDemo
 * @desc 功能描述
 * @by IDE: IntelliJ IDEA
 */
public class ChainedExceptionDemo {
    public static void method3() throws Exception{
        throw new Exception("异常来自method3()方法");
    }

    public static void method2() throws Exception{
        try{
            method3();
        }catch(Exception exc){
            throw new Exception("异常来自method2()方法",exc);
        }
    }

    public static void method1() throws Exception{
        try{
            method2();
        }catch(Exception ex){
            throw new Exception("异常来自method1()方法",ex);
        }
    }

    public static void main(String[] args) {
        try{
            method1();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

八、创建自定义异常类

可以通过继承Java.lang.Exception类来定义一个自定义异常类。

Java提供相当多的异常类,尽量使用它们而不要创建自己的异常类。如果遇到一个不能用预定义异常类来充分描述的问题,那就可以通过继承Exception类或其子类来创建自己的异常类。

/**
 * @author MNH
 * @version 1.0
 * @project Name: JAVA基础
 * @file Name: InvalidRadiusException
 * @desc 功能描述
 * @by IDE: IntelliJ IDEA
 */
public class InvalidRadiusException extends Exception{
    private double radius;

    public InvalidRadiusException(double radius){
        super("Invalid radius:"+radius);
        this.radius=radius;
    }

    public double getRadius(){
        return radius;
    }
}

这个自定义异常类继承自java.lang.Exception,而Exception类继承自java.lang.Throwable,Exception类中的所有方法(例如,getMessage()、toString()、printStackTrace())都是从Throwable继承而来。

Exception类的构造方法:
Exception():构建一个没有信息的异常
Exception(String message):构建一个给定信息的异常
Exception(String message,Exception cause):构建一个具有指定信息和子句的异常,这形成了一个链式异常

/**
 * @author MNH
 * @version 1.0
 * @project Name: JAVA基础
 * @file Name: TestCircleWithCustomException
 * @desc 功能描述
 * @by IDE: IntelliJ IDEA
 */
public class TestCircleWithCustomException {
    public static void main(String[] args) {
        try {
            CircleWithCustomException circle1 = new CircleWithCustomException(2);
            CircleWithCustomException circle2 = new CircleWithCustomException(-2);
            CircleWithCustomException circle3 = new CircleWithCustomException();
        } catch (InvalidRadiusException ex) {
            System.out.println(ex);
        }
        System.out.println("创建圆的个数:" + CircleWithCustomException.getNumberOfCircle());
    }
}

class CircleWithCustomException {
    private double radius;

    private static int numberOfCircle = 0;

    public CircleWithCustomException(double radius) throws InvalidRadiusException {
        setRadius(radius);
        numberOfCircle++;
    }

    public CircleWithCustomException() throws InvalidRadiusException {
        this(0);
    }

    public void setRadius(double radius) throws InvalidRadiusException {
        if (radius >= 0) {
            this.radius = radius;
        } else {
            throw new InvalidRadiusException(radius);
        }
    }

    public double getRadius() {
        return radius;
    }

    public static int getNumberOfCircle() {
        return numberOfCircle;
    }

    public double area() {
        return 3.14 * radius * radius;
    }
}

也可以继承RuntimeException声明一个自定义类异常,但这不是一个好方法,因为这会使自定义异常成为免检异常。最好使自定义异常是必检的,这样编译器就可以在程序中强制捕获这些异常。

本文地址:https://blog.csdn.net/julac/article/details/112607629

《一篇文章带你理解并掌握Java的异常.doc》

下载本文的Word格式文档,以方便收藏与打印。