JavaSE语法(15)—— 详细解读java中的异常

115 阅读11分钟

1.异常的概念与体系结构

1.1 异常的概念

  在Java中,将程序执行过程中发生的不正常行为称为异常。

  就比如下面这些:

  • 算数异常

image.png

  • 数组越界异常

image.png

  • 空指针异常

image.png

  • 其它异常

……

  这些从报错信息中可以看到,这些异常都是java.lang下的类。没错,异常其实也是类,Java内部维护了一个异常的体系结构。

1.2 异常的体系结构

异常类的体系结构.drawio.png

  • Throwable是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception。

  • Error 指的是Java虚拟机无法解决的严重问题,像JVM的内部错误、资源耗尽等。比如:StackOverflowErrorOutOfMemoryError

  • Exception异常产生后程序员可以通过代码进行处理,使程序继续执行。

1.3 异常的分类

  异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为编译时异常运行时异常

  • 运行时异常

  在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常。RunTimeException以及其子类对应的异常,都称为运行时异常。(这时编译器没有报错,可以通过编译。)

image.png

  • 编译时异常

  在程序编译期间必须要你处理的异常,称为编译时异常,也称为受检查异常。就是编译器要求你必须处理的异常,代码还没有运行,编码器就会检查你的代码,对可能出现的异常必须做出相对的处理(如果你不处理就会报错,比如下面的CloneNotSupportedException异常)。

image.png

2.异常的处理

2.1 异常的抛出(throw关键字)

  Java中的异常可以手动抛出,其格式如下:

  throw new XXXException("异常产生的原因");

  在我们编写方法时,如果传进来的参数不符合要求,就可以手动抛出异常来告诉调用者错在哪。

  实现一个获取数组中任意位置元素的方法。

public class Main {


    public static int getElement(int[] array, int index){

        if(null == array){
            throw new NullPointerException("传递的数组为null");
        } 
        
        if(index < 0 || index >= array.length){
            throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
        }
        
        return array[index];
    }
    

    public static void main(String[] args) {
        int[] array = {456};
        getElement(array, 3);
        System.out.println("这一行代码不会继续执行了");
    }    
}

结果:

image.png

注意:

  1. 抛出的对象必须是Exception 或者 Exception 的子类对象。
  2. 如果抛出的是 RunTimeException 或者RunTimeException的子类,则可以不用处理(编译器没有报错),直接交给JVM来处理。(JVM处理的方式就是中断程序,所以发生异常之后的代码就无法执行)
  3. 如果抛出的是编译时异常,用户必须处理,否则无法通过编译(下面详细介绍)。

2.2 异常的捕获

2.2.1 异常声明 throws 关键字

  编译时异常也是可以抛出的,但是如果不处理就无法通过编译。

  我们忽略具体业务,只抛出(new)一个编译时异常

public class Main {

    public static void main(String[] args) {
        TestException();
    }

    public static void TestException(){

            throw new FileNotFoundException("文件名字不对");
    }
}

image.png

  可以看到,这里跟前面的例子不同,这里的编译时异常 FileNotFoundException直接报错。那么这里的“处理”具体是怎么处理呢?

  这就要用到throws了。异常的具体处理方式,主要有两种:异常声明throws 以及 try-catch捕获处理,这里先介绍 throws

  如果我们想让上面的代码不报错,就得要加上throws声明,如下:

image.png

  可以看到 TestException() 方法中没有报错了,反而是main方法中报错了,这是为什么呢?这就是 throws 的作用,异常声明throws 的处理方式就是“甩锅”,就是把本方法中的异常甩到上一级方法中(就是调用者),让上一级方法处理。 这就间接的把异常给处理了。

  但是这还没完呀,TestException() 把锅甩到了main身上,但是main也没处理,main说:“我不干了,凭啥我来处理?” 于是编译器继续报错,不让你编译成功。main迫于压力不得不继续甩锅,也加上了throws声明:

public class Main {

    public static void main (String[] args) throws FileNotFoundException {
        TestException();
    }

    public static void TestException() throws FileNotFoundException{

            throw new FileNotFoundException("文件名字不对");
    }
}

image.png

   main 也加上了throws声明,这时编译器就不会报错,会让你编译成功。但是这么做就会把异常甩给JVM处理,上面提到过,JVM处理的方式就是结束程序。

结果:

image.png

  最后总结并补充一下:

  1.编译时异常和运行时异常都可以用throws来声明,其中的运行时异常没有要求,就算不用throws也不会报错。

  2.throws声明是放在方法参数列表之后,当方法中抛出异常时,用户不想处理该异常,此时就可以借助throws将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。

  3.方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。

public class Main{

    public static int getElement(int[] array, int index) throws NullPointerException,ArrayIndexOutOfBoundsException{

        if(null == array){
            throw new NullPointerException("传递的数组为null");
        }

        if(index < 0 || index >= array.length){
            throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
        }

        return array[index];
    }


    public static void main(String[] args) throws NullPointerException,ArrayIndexOutOfBoundsException {
        int[] array = {4,5,6};
        getElement(array, 3);
        System.out.println("这一行代码不会继续执行了");
    }
}

或者

public class Main {
    public static int getElement(int[] array, int index) throws RuntimeException{

        if(null == array){
            throw new NullPointerException("传递的数组为null");
        }

        if(index < 0 || index >= array.length){
            throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
        }

        return array[index];
    }

    public static void main(String[] args) throws RuntimeException {
        int[] array = {4,5,6};
        getElement(array, 3);
        System.out.println("这一行代码不会继续执行了");
    }
}

2.2.2 try-catch 捕获并处理

  其实使用 throws 并没有对异常进行一个真正的处理,如果只使用throws到最后还让 JVM来处理了,我们知道JVM的处理方式就是结束程序,这就有一个缺点:发生异常的那行代码之后的程序就不会再执行了。

  那么有什么办法来真正的处理异常,并且让程序继续执行下去?那就是 try-catch

  try-catch的语法结构 与 switch-case 相似:

public static void main(String[] args) {

    try{
        // 将可能出现异常的代码放在这里
    } catch (要捕获的异常类型 e){
        // 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类时,就会被捕获到
        // 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
    } catch (要捕获的异常类型 e){

    } catch (要捕获的异常类型 e){

    } .....{

    }

    // 后序代码
    // 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行
    // 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
}
  • 将可能出现异常的代码放在try里面。
  • catch 中用来处理已捕获的异常e

  上面的案例:

public class Main {

    public static void main (String[] args){
        TestException();
    }

    public static void TestException() throws FileNotFoundException {

        throw new FileNotFoundException("文件名字不对");
    }
}

image.png

   TestException()将异常甩给main方法,main:“行吧,脏活累活就交给我吧。”这时main就可以使用 try-catch来处理异常:

public class Main {

    public static void main (String[] args){
    
        try{
            //可能出现异常的代码放在这里
            TestException();
            //后续代码不会执行
            System.out.println("try块内抛出异常位置之后的代码将不会被执行");
        } catch (FileNotFoundException e){
            // 异常的处理方式,这一部分你想怎么处理就怎么处理。

            //System.out.println(e.getMessage()); // 只打印异常信息
            //System.out.println(e); // 打印异常类型:异常信息
            e.printStackTrace(); // 打印信息最全面
        }
        
        System.out.println("如果异常处理成功,这一行代码将继续执行。");
    }

    public static void TestException() throws FileNotFoundException {

        throw new FileNotFoundException("文件名字不对");
    }
}

结果:

image.png

  很显然,这样的处理方式才更加合理,达到了让程序继续执行下去的目的。但是要注意的是try块内抛出异常位置之后的代码将不会被执行。

  补充:

  1. 如果抛出异常类型与catch异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序。
public class Main {

    public static void main (String[] args){

        int[] arr = {1,2,3,4};

        try{

            int n = arr[10];
        } catch (NullPointerException e){

            e.printStackTrace();
        }
        System.out.println("如果异常处理成功,以下代码将继续执行。");
    }
}

结果:

image.png

  1. try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获。
public class Main {

    public static void main (String[] args){

        int[] arr = {1,2,3,4};

        try{
            //越界
            int n = arr[10];
            //空指针
            arr = null;
            n = arr[10];
        } catch (NullPointerException e){
            
            e.printStackTrace();
        } catch (IndexOutOfBoundsException e){

            e.printStackTrace();
        }
        System.out.println("如果异常处理成功,以下代码将继续执行。");
    }
}

结果:

屏幕截图 2023-03-13 121049.png

3.catch进行类型匹配的时候, 不光会匹配相同类型的异常对象, 也会捕捉目标异常类型的子类对象。

public class Main {

    public static void main (String[] args){

        int[] arr = {1,2,3,4};

        try{

            //越界
            int n = arr[10];
            //空指针
            arr = null;
            n = arr[10];
        } catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("如果异常处理成功,以下代码将继续执行。");
    }
}

结果:

屏幕截图 2023-03-13 121049.png

4.finally关键字。有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。

其语法格式:

public class Main {
    public static void main (String[] args){
        
        try{
        // 可能会发生异常的代码
        }catch(异常类型 e){
        // 对捕获到的异常进行处理
        }finally{
        // 此处的语句无论是否发生异常,都会被执行到
        } 
        // 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
    }
}

案例:

public class Main {

    public static void main (String[] args){

        int[] arr = {1,2,3,4};

        try{
            System.out.println("进入try中");
            int n = arr[10];
        } catch (NullPointerException e){

            e.printStackTrace();
        } finally {
            System.out.println("这一行一定会执行");
        }
        System.out.println("如果异常处理成功,以下代码将继续执行。");
    }
}

结果:注意代码的执行顺序。

image.png

5.finallytry-catch-finally 后的代码都会执行,那为什么还要有finally呢?

public class Main {

    public static void main (String[] args){


        Scanner in = null;
        try{
        
            in = new Scanner(System.in);
            //输入一个数并打印
            int n = in.nextInt();
            System.out.println(n);
            return;
        } catch (InputMismatchException e){
            e.printStackTrace();
        } finally {
            System.out.println("finally中代码");
        }

        System.out.println("释放资源");
        if(null != in){
            in.close();
        }
    }
}

结果:

image.png

  如果在try中加了return,正常情况下,try-catch-finally后面的代码是执行不到的,然而finally却是能执行得到,所以有finally存在的必要性,一般在finally中进行一些资源释放的工作。

2.3 异常的处理流程

  1. 程序先执行try中的代码。
  2. 如果try中的代码出现异常, 就会结束try中的代码, 看和catch中的异常类型是否匹配。
  3. 如果找到匹配的异常类型, 就会执行catch中的代码。
  4. 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者。
  5. 无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行)。
  6. 如果上层调用者也没有处理的了异常, 就继续向上传递。
  7. 一直到main方法也没有合适的代码处理异常, 就会交给JVM来进行处理, 此时程序就会异常终止。

3.自定义异常类

  Java给我们提供了很多的异常类,但是并不能完全表示实际开发中所遇到的一些异常,这就需要我们自己定义异常。

  用户登录案例:

public class Login {

    private static String userName = "李四";
    private static String password = "123456abc";


    public static void loginInfo(String user, String pass) {

        if(!user.equals(userName)){
            System.out.println("用户名错误!");
            return;
        }
        if(!pass.equals(password)){
            System.out.println("密码错误!");
            return;
        }
        System.out.println("登陆成功");
    }

    public static void main(String[] args) {
        loginInfo("李四","123456");
    }
}

结果:

image.png

  如果我们想让错误更明显,错误信息更详细,我们可以自己定义用户名错误异常以及密码错误异常

  自己定义的异常必须要继承Exception 或者 RuntimeException 类,继承自 Exception 的异常默认是受查异常;继承自RuntimeException的异常默认是非受查异常。

public class PasswordException extends Exception {
    
    //实现一个带有String类型参数的构造方法,参数含义:出现异常的原因。
    public PasswordException(String message){
        super(message);
    }
}
public class UserNameException extends Exception{

    //实现一个带有String类型参数的构造方法,参数含义:出现异常的原因。
    public UserNameException(String message){
        super(message);
    }
}
public class Login {

    private static String userName = "李四";
    private static String password = "123456abc";


    public static void loginInfo(String user, String pass)throws UserNameException,PasswordException {

        if(!user.equals(userName)){
            throw new UserNameException("用户名错误!");
        }
        if(!pass.equals(password)){
            throw new PasswordException("密码错误!");
        }
        System.out.println("登陆成功");
    }

    public static void main(String[] args) {
        try{
            loginInfo("李四","123456");
        } catch(UserNameException e){
            e.printStackTrace();
        } catch (PasswordException e){
            e.printStackTrace();
        }
    }
}

结果:

image.png