浅析Java中的异常机制

222 阅读8分钟

异常机制

异常也就是正常的反义词,顾名思义,异常指出现了一些正常情况之外的一些状况,导致事情无法顺利的进行下去。Java中的异常是指程序在执行期间发生的时间中断了正在执行程序的正常指令流,当程序执行遇到异常时,通常需要解决掉出现的异常才能保证程序得到想要的结果

没有一个程序可以完美到从敲下的第一个方法在最后运行结束都没有一点错误,因此,学会分辨程序中出现的异常以及找到导致异常的程序,最后合理的解决异常就变得极为重要。常见的异常比如,数组越界异常(ArrayIndexOutOfBoundsException)、空指针异常(NullPointerException)、类型转换异常(ClassCastException)……当出现这些异常的时候,我们可以根据异常的类型和打印的异常信息知道该如何解决。以及,有时为了业务中特殊的业务逻辑需要自定义异常,从而方便代码的调试和维护。

Java中所有异常类型都是java.lang.Throwable的子类,Throwable和其子类的关系图如下所示:
在这里插入图片描述

  • Error: 指运行程序中较为严重的问题,此时程序无法自行进行处理,大多数的Error和Java虚拟机有关,如常见的堆栈溢出、JVM运行错误等

  • Exception: 指运行程序可以自行处理的异常,通常又可以分为两类:

    • 编译期异常:进行编译Java代码程序出现的问题,如果没有处理异常会导致编译失败
    • 运行期异常(RunTimeException):Java程序运行过程中出现的问题,如索引越界异常

例如,我们使用SimpleDateFormat类解析某个字符串是否符合定义的格式,如果正常则不会报错,否则系统就会抛出java.text.ParseException

public class ThrowableMain {
    // public static void main(String[] args) throws ParseException {} 抛出异常
    public static void main(String[] args){
        SimpleDateFormat sdf =  new SimpleDateFormat("yyyy-MM-dd");
        Date date = sdf.parse("1999-0909");
    }
}

那么异常在程序的运行过程中是如何出现的呢?下面我们通过一个Demo来看一下具体的流程,假设,此时有一个int型数组,我们想要写一个函数实现根据给定的索引取出对应位置的元素,代码如下所示:

public class ExceptionMain {
    public static void main(String[] args) {
		int[] array = new int[]{1,2,3};
		System.out.println(getElement(array, 0)); // 1
		System.out.println(getElement(array, 4)); // java.lang.ArrayIndexOutOfBoundsException: 4
    }
    public static int getElement(int[] arr, int index){
        int ele = arr[index];
        return ele;
    }
}

当执行第一条输出语句时,程序可以得到正确的结果1;当执行第二条输出语句时,系统就会抛出数组越界异常,因为数组的长度为3,索引为4时显然就越界了。那么在程序的运行中,ArrayIndexOutOfBoundsException是如何被抛出的呢?下面通过图来直观的理解一下:
在这里插入图片描述

异常的处理

当出现异常后,我们必须针对具体的异常进行处理,常用的异常处理有两种方法:

throws:

  • 当调用的方法抛出异常对象时,使用throws关键字处理异常对象,它会把异常对象声明抛出给方法的调用者处理,最终交给JVM处理。使用格式为:

     修饰符 返回值类型 方法名(参数列表) throws xxxException...{}
    

    例如,使用throws来解决之前的ParseException:

    public class ThrowableMain {
        public static void main(String[] args) throws ParseException {
            SimpleDateFormat sdf =  new SimpleDateFormat("yyyy-MM-dd");
            Date date = sdf.parse("1999-0909");
    
        }
    }
    

    main方法通过throws字自己不处理异常,而是将其交给JVM处理,最终JVM就会打印出如下信息:

    Exception in thread "main" java.text.ParseException: Unparseable date: "1999-0909"
    at java.text.DateFormat.parse(DateFormat.java:366)
    at Throwable.ThrowableMain.main(ThrowableMain.java:18)
    

    注意事项:

    • throws关键字必须写在方法的声明处
    • throws关键字后边声明的异常必须是Exception或者Exception的子类
    • 方法内部如果抛出了多个异常对象,那么直接声明父类异常即可,也就是说如果抛出的多个异常对象有子父类关系,那么直接声明父类异常即可
    • 调用了一个声明抛出异常的方法,我们就必须处理声明的异常。要么继续使用throws声明抛出,交给方法的调用者处理,最终交给JVM,要么try-catch自己处理

try-catch

try-catch机制表示程序自己捕获异常然后处理异常。使用格式为:

try{
	可能产生异常的代码
}catch (定义一个异常的变量,用来接收try中抛出的异常对象){
	异常的处理逻辑,声明异常对象后怎么处理
	一般在工作中,会把异常对象打log
}

注意事项:

  • try 中可能会抛出多个异常对象,那么就可以使用多个catch来处理异常对象
  • 如果try中产生了异常,那么就会执行catch中的异常处理逻辑,执行完毕catch中的处理逻辑,继续执行try-catch之后的代码
  • 如果try中没有产生异常,那么就不会执行catch中的处理逻辑,执行完try中的代码,继续执行try-catch之后的代码
  • 无论如何最多只有一个catch被执行,有可能所有的catch都不被执行,即程序不抛出异常

那么使用try-catch如何处理抛出的多个异常对象呢?常用的有三种:

  • 多个异常分别处理

    public class ExceptionMain {
        public static void main(String[] args) {
            try{
                int[] array = new int[]{1,2,3};
                System.out.println(array[4]);
            } catch ( ArrayIndexOutOfBoundsException e){
                e.printStackTrace();
                /*
                java.lang.ArrayIndexOutOfBoundsException: 4
    	        at Throwable.ExceptionMain.main(ExceptionMain.java:24)
                 */
            }
            
        public static int getElement(int[] arr, int index){
            int ele = arr[index];
            return ele;
        }
    }
    
  • 多个异常一次捕获,多次处理

    public class ExceptionMain {
        public static void main(String[] args) {
            /*
             一个try多个catch的注意事项:
                catch里定义的异常变量,如果有子父类关系,那么子类的异常变量必须写在上面,否则会报错
                ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException
             */
            try{
                int[] array = new int[]{1,2,3};
                System.out.println(array[4]);
    
                List<Integer> list = new ArrayList<>();
                list.add(1);
                list.add(2);
                list.add(3);
                System.out.println(list.get(4));
            } catch (ArrayIndexOutOfBoundsException e){
                e.printStackTrace();
            } catch (IndexOutOfBoundsException e){
                e.printStackTrace();
            }
        }
        public static int getElement(int[] arr, int index){
            int ele = arr[index];
            return ele;
        }
    }
    
  • 多个异常一次捕获一次处理

    public class ExceptionMain {
        public static void main(String[] args) {
            try{
                int[] array = new int[]{1,2,3};
                System.out.println(array[4]);
    
                List<Integer> list = new ArrayList<>();
                list.add(1);
                list.add(2);
                list.add(3);
                System.out.println(list.get(4));
            } catch (Exception e){
                e.printStackTrace();
            }
        }
        public static int getElement(int[] arr, int index){
            int ele = arr[index];
            return ele;
        }
    }
    

Throwable类

常用方法:

  • public string getMessage(): 返回异常发生时的详细信息

  • public string toString(): 返回异常发生时的简要描述

  • public string getLocalizedMessage(): 返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同

  • public void printStackTrace(): 在控制台上打印Throwable对象封装的异常信息

    public class ExceptionTest {
        public static void main(String[] args) {
            try {
                int[] array = new int[]{1, 2, 3};
                System.out.println(array[4]);
            }catch (ArrayIndexOutOfBoundsException e){
                System.out.println(e.getMessage());  // 4
                System.out.println(e.toString());  // java.lang.ArrayIndexOutOfBoundsException: 4
                e.printStackTrace();
                // java.lang.ArrayIndexOutOfBoundsException: 4
                // at Throwable.ExceptionTest.main(ExceptionTest.java:7)
            }
    
        }
        public static int getElement(int[] arr, int index){
            if (index > arr.length){
                throw new ArrayIndexOutOfBoundsException();
            }
            int ele = arr[index];
            return ele;
        }
    }
    

finally

finally子句无论是否出现异常都会执行:

  • finally不能单独用,必须和try一起使用
  • finally一般用于资源释放,无论程序是否出现异常,最后都要资源释放
  • 如果finally中有return语句,那么永远返回finally中的结果

try、catch、finally语句块的执行顺序:

  • 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
  • 当try捕获到异常,catch语句块里没有处理此异常的情况:此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
  • 当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句。

子父类异常

异常机制中同样存在于类的继承中,那么在类的继承中如果处理父类和子类中的异常对应呢?通常来说,如果父类抛出了多个异常,则类重写父类方法时,抛出和父类相同的异常或者父类异常的子类或者不抛出异常。 父类没有抛出异常,子类重写父类该方法时也不可抛出异常,此时子类产生该异常,只能捕获处理,不能声明抛出。

public class Fu {
    public void show1() throws NullPointerException, ClassCastException{}
    public void show2() throws IndexOutOfBoundsException{}
    public void show3() throws IndexOutOfBoundsException{}
    public void show4(){}
}

class zi extends Fu{
    // 抛出和父类相同的异常
    public void show1()throws NullPointerException, ClassCastException{}

    // 抛出父类异常的子类
    public void show2() throws ArrayIndexOutOfBoundsException{}

    // 不抛出异常
    public void show3(){}

    // 父类没有抛出异常,子类抛出异常需自行处理
    public void show4(){
        try{
            throw new Exception();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

自定义异常

前面说到,有时根据具体的业务需求需要设计自己的异常类来满足需求。如果要自定义异常类,则扩展Exception类即可,自定义的异常应该总是包含如下的构造函数:

  • 一个无参构造函数
  • 一个带有String参数的构造函数,并传递给父类的构造函数
  • 一个带有String参数和Throwable参数,并都传递给父类构造函数
  • 一个带有Throwable 参数的构造函数,并传递给父类的构造函数
public class MyException extends Exception{
    public MyException(){}

    public MyException(String message) {
        super(message);
    }

    public MyException(String message, Throwable cause) {
        super(message, cause);
    }

    public MyException(Throwable cause) {
        super(cause);
    }

    public MyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}