Java 中的异常(1万字超全详解)

异常

1. 异常的引入

案例,运行下面代码,看看有什么问题。

public class Exception01 {
    
    
    public static void main(String[] args)  {
    
    
    	System.out.println("程序开始运行....");
    	
        int num1 = 10;
        int num2 = 0;
        int res = num1 / num2;
        
        System.out.println("程序继续运行....");

    }
}
  • 代码分析:

0. 输出 程序开始运行…;
1. num1 / num2 => 10 / 0;
2. 当执行到 num1 / num2 因为 num2 = 0, 程序就会出现(抛出)异常 ArithmeticException;
3. 当抛出异常后,程序就退出,崩溃了 , 下面的代码不再执行(不输出程序继续运行…);
4. 大家想想这样的程序好吗? 不好,不应该出现了一个不算致命的问题,就导致整个系统崩溃;

  • java 设计者,提供了一个叫 异常处理机制来解决该问题。如果程序员,认为一段代码可能出现异常/问题,可以使用try-catch异常处理机制来解决,从而保证程序的健壮性。

  • 在IDEA 中,将该代码块->选中->快捷键 ctrl + alt + t -> 选中 try-catch; 如果进行异常处理,那么即使出现了异常,程序也可以继续执行下去。

  • 代码优化:

public class Exception01 {
    
    
    public static void main(String[] args)  {
    
    
		System.out.println("程序开始运行....");
        int num1 = 10;
        int num2 = 0;

        try {
    
    
            int res = num1 / num2;
        } catch (Exception e) {
    
    
            e.printStackTrace();// 这条语句是系统默认有的,可以省略;
        }

        System.out.println("程序继续运行....");// 可以执行该语句;

    }
}
  • 可以发现,在加入异常处理机制后,程序会完整执行,在出现异常位置之后的语句也正常输出了;原本系统会报错的部分则会在最后输出。

2. 异常的基本概念

2.1 异常的概念

  • 如下图:
    在这里插入图片描述
  • 说明:

(1)区分“异常事件”和“异常”,“异常事件”分为两大类,分别是“错误”(Error)和“异常”(Exception);我们一般说的“异常”指的是 Exception 。

(2)“错误”(Error)是程序员无法通过代码来控制的事情,在本篇不会介绍如何处理错误,而只解释如何处理异常。
(3)“异常”(Exception)又分为两类,一种是编译时异常,一种是运行时异常;这两种异常程序员都可以通过代码来处理。。

(4)编译时异常,即程序在编译时产生的异常;编译器会直接检测出来是否存在编译时异常,出现了编译时异常的程序编译器根本编译不了,程序员必须直接处理,再运行程序。
(5)运行时异常,即程序在运行时才产生的异常;编译器不能检测出来程序是否存在运行时异常,在程序运行时才能知道是否会出现运行时异常;程序员应该尽量避免运行时异常。上面的程序中出现的就是运行时异常。
(6)对于运行时异常,程序员可以不做处理,因为这类异常很普遍,若全部处理可能会对程序的可读性和运行效率产生影响。

2.2 异常的体系图

  • 在Java 中,所有可能出现的“异常事件”对应了各个异常事件类,程序员通过调用异常事件类中的方法来查看和解决“异常事件”。
  • 异常类体系图如下:
    在这里插入图片描述
  • 异常类体系图说明:

(1)所有“异常事件”类的最高父类是 Throwable 类,它继承了 Object 类。
(2)Error 类对应了“错误”;Exception 类则对应了“异常”,这里只讨论 Exception 类

(3)Exception 类又分成了两类:RuntimeException 类对应了“运行时异常”;需要注意,“编译时异常”则直接继承自 Exception 类,和 RuntimeException 类是同级类。

(4)RuntimeException 类下面则是多个具体的运行时异常的子类;比如我们开篇举例的 num1 / num2 所出现的运行时异常,就是 ArithmicException 类。继承自 RuntimeException 类。

  • 下面开始重点讨论 Exception 类的内容。

3. 运行时异常

  • 运行时异常,即程序在运行时才产生的异常;编译器不能检测出来程序是否存在运行时异常,在程序运行时才能知道是否会出现运行时异常;程序员应该尽量避免运行时异常。
  • 常见的运行时异常包括:
运行时异常类 解释
NullPointerException 空指针异常
ArithmeticException 数学运算异常
ArrayIndexOutOfBoundsException 数组下标越界异常
ClassCastException 类型转换异常
NumberFormatException 数字格式不正确异常

3.1 NullPointerException 空指针异常

  • 当应用程序试图在需要对象的地方使用 null 时,抛出该异常。

  • 代码说明:

public class NullPointerException_ {
    
    
    public static void main(String[] args) {
    
    

        String name = null;
        System.out.println(name.length()); // 抛出 NullPointerException
    }
}

3.2 ArithmeticException 数学运算异常

  • 当出现异常的运算条件时,抛出此异常。例如,用一个整数“除以0”时,抛出该异常。
  • 代码说明:
public class ArithmeticException_ {
    
    
    public static void main(String[] args)  {
    
    
    	
        int num1 = 10;
        int num2 = 0;
        int res = num1 / num2; // 抛出 ArithmeticException
        
    }
}

3.3 ArrayIndexOutOfBoundsException 数组下标越界异常

  • 用非法索引访问数组时抛出的异常;如果索引为负或大于等于数组大小,则该索引为非法索引。
  • 代码说明:
public class ArrayIndexOutOfBoundsException_ {
    
    
    public static void main(String[] args) {
    
    
        int[] arr = {
    
    1,2,4};
        for (int i = 0; i <= arr.length; i++) {
    
    
            System.out.println(arr[i]);// 这里抛出ArrayIndexOutOfBoundsException
        }
    }
}

3.4 ClassCastException 类型转换异常

  • 当试图将对象强制转换为不是实例的子类时,抛出该异常。
  • 代码说明:
public class ClassCastException_ {
    
    
    public static void main(String[] args) {
    
    
        A b = new B(); // 向上转型
        B b2 = (B)b;// 向下转型,这是正确的
        C c2 = (C)b;// 抛出 ClassCastException
    }
}

class A {
    
    } // 父类
class B extends A {
    
    } // 子类
class C extends A {
    
    } // 子类

3.5 NumberFormatException 数字格式不正确异常

  • 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常; => 使用给异常我们可以确保输入的是满足条件数字。
  • 代码说明:
public class NumberFormatException_ {
    
    
    public static void main(String[] args) {
    
    
    
        String name = "韩顺平教育";
        String age = "1234";
        // 将String 转成 int
		int num1 = Integer.parseInt(age);// 不会抛出异常	
		System.out.println(num1);
		
        int num2 = Integer.parseInt(name);// 抛出 NumberFormatException
        
    }
}

4. 编译时异常

  • 编译时异常,即程序在编译时产生的异常;编译器会直接检测出来是否存在编译时异常,出现了编译时异常的程序编译器根本编译不了,程序员必须直接处理,再运行程序。
  • 常见的编译时异常包括:
    在这里插入图片描述
  • 这里以 FileNotFoundException 类举例,其他编译时异常类似。
  • 代码举例如下:
public class Exception02 {
    
    
    public static void main(String[] args) {
    
    

        try {
    
    
            FileInputStream fis;
            fis = new FileInputStream("d:\\aa.jpg");// 不存在该文件,编译器可以检测到,必须直接处理;
            int len;
            while ((len = fis.read()) != -1) {
    
    
                System.out.println(len);
            }
            fis.close();
        } catch (IOException e) {
    
    
            e.printStackTrace();// 处理该异常,程序可以被编译运行。
        }
    }
}

5. 异常处理

  • 异常处理:就是当异常(Exception)发生时,处理异常的方式。
  • 异常处理的两种方式:

(1)try-catch :程序员在程序中捕获发生的异常,直接处理。
(2)throws :不直接处理异常,而是将发生的异常抛出,(一层一层向上)交给调用者(方法)来处理,最顶级的处理者就是JVM 。(看例子理解)

5.1 try-catch 异常处理

  • Java 提供try 和 catch 块来直接处理发生的异常。try 块用于包含可能发生异常的代码;catch 块用于处理 try 块中发生的异常;可以根据需要在程序中使用多个 try-catch 块。

5.1.1 try-catch 基本用法

  • 基本语法:

        try {
    
    
            // 此处包含可能会发生异常的代码,包括运行时、编译时异常;
            // 如果此处的代码块发生了异常,将其生成对应的异常类对象,作为实参传给catch 块;
        } catch (Exception e) {
    
    
            e.printStackTrace();
            // Java默认提供的处理异常的语句,可以另外添加其他语句,
            // 也可以省略该语句。
        } finally {
    
    
			// 还可以添加 finally 块,finally 块中的语句必须执行。
		}
    }
    
  • 代码举例:
public class ArithmeticException_ {
    
    
    public static void main(String[] args)  {
    
    
    	
        int num1 = 10;
        int num2 = 0;
        
        try {
    
    
            int res = num1 / num2; // 生成 ArithmeticException 类对象,传给catch 块
            
        } catch (Exception e) {
    
    
            e.printStackTrace();
            System.out.println("程序出现了异常");// 自定义异常处理方法
        } 
    }
}

5.1.2 try-catch 处理机制示意图

  • 如下图:
    在这里插入图片描述

5.1.3 try-catch 注意事项和细节

(1)在 try 块中,如果某条语句发生了异常,则会直接跳到 catch 块中处理异常,不会再执行该异常语句后的代码;处理完异常后,跳出 catch 块,继续执行程序下面剩余的代码。
(2)如果 try 块中的语句全部没有发生异常,则顺序执行完 try 块中的语句,不会进入 catch 块中。
(3)如果希望无论是否发生异常,都执行某段代码(比如释放资源等),则可以在catch 块后 添加 finally 块,在 fanlly 块中写入始终执行的代码。

  • 代码举例:
public class TryCatchDetail {
    
    
    public static void main(String[] args) {
    
    

        // 1. 如果异常发生了,则异常发生后面的代码不会执行,直接进入到catch块
        // 2. 如果异常没有发生,则顺序执行try的代码块,不会进入到catch
        // 3. 如果希望不管是否发生异常,都执行某段代码(比如关闭连接,释放资源等)则使用 finally 块
        try {
    
    
        	// 可能会发生异常的代码:
            String str = "韩顺平";
            int a = Integer.parseInt(str);
            System.out.println("数字:" + a);
            
        } catch (NumberFormatException e) {
    
    
            System.out.println("异常信息=" + e.getMessage());// 处理异常
            
        } finally {
    
    
            System.out.println("finally代码块被执行...");// 无论是否发生异常,都会执行这段代码;
        }

		// 在处理完异常后,继续执行程序剩下的代码;
        System.out.println("程序继续执行...");
        // 如果没有使用 try-catch 处理异常,若发生了异常,则程序剩余的代码不会被执行。

    }
}

(4)一段程序可以有多个 catch 块,用于捕获不同的异常类,但要求父类异常必须在后,子类异常在前(比如 Exception 类在后,NullPointerException 类在前);如果发生了子类异常,则只会匹配子类的 catch 块用于处理异常。

  • 代码举例:

public class TryCatchDetail02 {
    
    
    public static void main(String[] args) {
    
    

        //1.如果try代码块有可能有多个异常
        //2.可以使用多个catch 分别捕获不同的异常,相应处理
        //3.要求子类异常写在前面,父类异常写在后面
        try {
    
    

            Person person = new Person();
            person = null;
            System.out.println(person.getName());// 抛出 NullPointerException类异常
            int n1 = 10;
            int n2 = 0;
            int res = n1 / n2;// 抛出 ArithmeticException类异常

        } catch (NullPointerException e) {
    
    
            System.out.println("空指针异常=" + e.getMessage());

        } catch (ArithmeticException e) {
    
    
            System.out.println("算术异常=" + e.getMessage());

        } catch (Exception e) {
    
    

            System.out.println("其他类异常" + e.getMessage());

        } 
    }
}

class Person {
    
    

    private String name = "jack";

    public String getName() {
    
    
        return name;
    }
}

(5)在没有编译时异常发生时,可以进行 try - finally 块配合使用,这种用法相当于没有捕获异常,因此如果发生运行时异常程序会直接中断退出。应用场景:就是执行一段代码,无论是否发生异常,都必须执行某个业务逻辑。

  • 代码举例:
public class TryCatchDetail03 {
    
    
    public static void main(String[] args) {
    
    

        try{
    
    
            int n1 = 10;
            int n2 = 0;
            System.out.println(n1 / n2);// 抛出异常,但没有 catch处理,直接进入 finall块

        }finally {
    
    
            System.out.println("执行了finally..");// 此语句一定会执行
        }

        // 由于没有处理异常,程序会中断退出,不会执行下面代码
        System.out.println("程序继续执行..");
    }
}

(6)在 try-catch-finally 块中,如果没有出现异常,则先执行try 块中的所有语句,不执行 catch 块中的语句;最后一定要执行 finally 块中的语句。
(7)在 try-catch-finally 块中,如果出现异常,则 try 块中异常语句后的代码不再执行,直接跳到 catch 块中执行;最后一定还要执行 finally 块中的语句。

(8)优先级最高的是 finally 块;即使 try 或 catch 语句块中有 return 语句,也是先执行完 finally 块中的语句,再回到 try 或 catch 块中执行 return 语句 (注意,return 后的语句的变量,系统会用一个临时变量保存),但如果 finally 块中同样有 return 语句,则会直接退出方法,不再回到try 或 catch 块。

  • 代码举例:
class ExceptionExe01 {
    
    

	public static void main(String[] args) {
    
    

        System.out.println(method());
        // 输出:i = 4
        //      3
    }
    
    public static int method() {
    
    

        int i = 1;// i = 1
        try {
    
    

            i++;// i = 2
            String[] names = new String[3];
            if (names[1].equals("tom")) {
    
     // 空指针异常
                System.out.println(names[1]);
            } else {
    
    
                names[3] = "hspedu";
            }
            return 1;

        } catch (ArrayIndexOutOfBoundsException e) {
    
    
            return 2;

        } catch (NullPointerException e) {
    
    
            return ++i;  // 执行++i,i = 3 => 保存临时变量 temp = 3;但不执行return
			// 执行完 finally 块后再回来执行 return;
        } finally {
    
    
            ++i; //i = 4
            System.out.println("i=" + i);// i = 4
        }
    }
}

5.2 throws 异常处理

5.2.1 throws 基本用法

(1)如果一个方法中的语句执行时可能产生某种异常,但是并不能确定如何处理这种异常(try-catch),则该方法标签中应该声明抛出该异常,表明该方法将不直接对这些异常进行处理,而是交由该方法的调用者负责处理。
(2)在方法标签的声明中用 throws 关键字可以声明抛出异常的列表(可以同时抛出多种异常),throws 后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

  • 代码举例:

public class Throws01 {
    
    
    public static void main(String[] args) {
    
    

    }

    public void f2() throws FileNotFoundException,NullPointerException,ArithmeticException {
    
    
        
        //创建了一个文件流对象
        //1. 这里的异常是一个FileNotFoundException 编译异常
        //2. 可以使用前面讲过的 try-catch-finally
        //3. 或者使用throws ,抛出异常, 让调用f2方法的调用者(方法)处理
        //4. throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类
        //5. throws 关键字后也可以是 异常列表, 即可以抛出多个异常
        FileInputStream fis = new FileInputStream("d://aa.txt");

    }
}

5.2.2 throws 处理机制示意图

  • 如下图:
    在这里插入图片描述

5.2.3 throws 注意事项和细节

(1)对于编译时异常,程序中必须处理,可以使用 try-catch 或者 throws 处理;但是要注意:使用 throws 抛出时,一定要在产生异常的方法标签中显式地声明 throws 抛出(一定要写出来)。
(2)对于运行时异常,不一定要处理;若方法中产生运行时异常且没有处理时,默认在该方法标签中隐式地声明了 throws 抛出处理,最上层会抛给 JVM来处理。

  • 代码举例:
public class ThrowsDetail {
    
    

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

    public static void f2() /*隐含:throws ArithmeticException*/ {
    
    
        //1.对于编译异常,程序中必须处理,比如 try-catch 或者 throws
        //2.对于运行时异常,程序中如果没有处理,默认就是throws的方式处理

        int n1 = 10;
        int n2 = 0;
        double res = n1 / n2;
    }

    public static void f1() throws FileNotFoundException {
    
    

        //1. f3() 方法抛出的是一个编译异常
        //2. 即这时,就要f1() 必须处理这个编译异常
        //3. 在f1() 中,要么 try-catch-finally ,或者继续throws 这个编译异常
        f3(); // 抛出异常
    }
    public static void f3() throws FileNotFoundException {
    
    
        FileInputStream fis = new FileInputStream("d://aa.txt");
    }

    public static void f4() {
    
    
        //1. 在f4()中调用方法f5() 是可以
        //2. 原因是f5() 抛出的是运行时异常
        //3. 而java中,并不要求程序员显式地处理,因为默认throws的方式处理机制
        f5();
    }
    public static void f5() throws ArithmeticException {
    
    

    }
}

(3)子类重写父类的方法时,若父类方法抛出了异常,则子类重写的方法也要抛出异常,且子类方法抛出的异常类型要么和父类方法抛出的一致,要么为父类抛出异常的子类型。

  • 代码举例:
class Father {
    
     //父类
    public void method() throws RuntimeException {
    
    
    }
}

class Son extends Father {
    
    //子类
    //3. 子类重写父类的方法时,对抛出异常的规定:子类重写的方法,
    //   所抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出的异常类型的子类型
    @Override
    public void method() throws ArithmeticException {
    
    
    }
}

(4)在异常向上抛出的过程中,若有方法使用 try-catch 处理了该异常,就不用再向上继续抛出了。

6. 自定义异常

6.1 自定义异常的基本概念

  • 当程序中出现了某些异常,但该异常信息并没有在 Throwable 子类中包含,这个时候可以自己设计异常类,用于描述该异常信息。(注意:自定义异常是异常类,并没有处理异常)

  • 基本定义步骤:

(1)定义异常类:自定义异常类的类名,继承 Exception 类或者 RuntimeException 类。
(2)如果继承了 Exception 类,则属于编译时异常类。
(3)如果继承了 RuntimeException 类,则属于运行时异常类(一般都是继承RuntimeException 类)。

  • throws 关键字:用于显式地抛出一个自定义异常对象(提示此处会存在一个自定义异常),该对象可以 被 try-catch 捕获并处理,或者被 throws 抛出。

6.2 自定义异常的应用案例

  • 案例如下:
    在这里插入图片描述
  • 代码实现:
public class CustomException {
    
    
    public static void main(String[] args) /*隐含:throws AgeException*/ {
    
    

        int age = 180;
        
        //要求范围在 18 – 120 之间,否则抛出一个自定义异常
        if(!(age >= 18 && age <= 120)) {
    
    
            //这里我们可以通过构造器,设置异常信息
            throw new AgeException("年龄需要在 18~120之间");
            // 此处抛出了一个自定义异常对象,使用默认的 throws 处理该异常;
        }
        System.out.println("你的年龄范围正确.");
    }
}

//自定义一个异常类
//1. 一般情况下,我们自定义异常是继承 RuntimeException
//2. 即把自定义异常做成 运行时异常,好处是,我们可以使用默认的处理机制
class AgeException extends RuntimeException {
    
    

    //构造器
    public AgeException(String message) {
    
    
        super(message);
    }
}

6.3 throw 和 throws 的区别

  • 如下图:
    在这里插入图片描述

总结

  • 本文是小白博主在学习B站韩顺平老师的Java网课时整理总结的学习笔记,在这里感谢韩顺平老师的网课,如有有兴趣的小伙伴也可以去看看。
  • 本文详细解释了 异常 的概念与使用,并深入讲解了 异常 使用的注意事项和细节,还举了很多很多例子,希望小伙伴们看后能有所收获!
  • 最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!我们下一篇博文见吧!

猜你喜欢

转载自blog.csdn.net/weixin_45395059/article/details/125740684