java学习 -06 认识异常

一、异常的介绍

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

  2. Java中,描述异常是根据类来进行描述的。例如:

    • 算术异常

      System.out.println(3/0);
      //执行结果
      //Exception in thread "main" java.lang.ArithmeticException: / by zero
      //at Test.main(Test.java:10)
      

    • 数组越界异常

      int[] arr = {1,2,3};
      System.out.println(arr[10]);
      //执行结果
      //Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 3
      //at Test.main(Test.java:11)
      

    • 空指针异常

      int[] arr = null;
      System.out.println(arr.length);
      //执行结果
      //Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "arr" is null
      //at Test.main(Test.java:11)
      

    可知,Java中不同类型的异常,都有与其对应的类来进行描述。

  3. 异常的体系结构

    由图可知:

    • Throwable:是异常体系的顶层类,其派生出两个重要的子类,ErrorException
    • Error:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError,一旦发生回力乏术。
    • Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。
  4. 异常的分类

    • 编译时异常(Checked Exception):在程序编译期间发生的异常,也称为受检查异常。

      class Person {
          private String name;
      
          @Override
          protected Object clone() {
              return (Person)super.clone();
          }
      }
      //编译时报错
      //Java: 未报告的异常错误java.lang.CloneNotSupportedException; 必须对其进行捕获或声明以便抛出
      

    • 运行时异常(Unchecked Exception):程序执行期间发生的异常,也称为非受检查异常。RunTimeException以及其子类对应的异常,都称为运行时异常。比如:NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException。

    注意:编译时出现的语法性错误,不能称之为异常。

二、异常的处理

  1. 防御式编程

    主要的方式:

    • LBYL(Look Before You Leap):事前防御型

      // 防御性检查:确保用户名和密码不为null且不为空
      if (username == null || username.trim().isEmpty()) {
          throw new IllegalArgumentException("Username cannot be null or empty");
      }
      if (password == null || password.isEmpty()) {
          throw new IllegalArgumentException("Password cannot be null or empty");
      }
      

      缺陷:正常流程和错误处理流程代码混在一起,代码整体显得比较混乱。

    • EAFP(It's Easier to Ask Forgiveness than Permission):事后认错型

      try {
          int[] numbers = {1,2,3};
          System.out.println(numbers[10]);
      } catch (ArrayIndexOutOfBoundsException e) {
          System.out.println("捕获到数组越界异常:" + e.getMessage());
      }
      //执行结果
      //捕获到数组越界异常:Index 10 out of bounds for length 3
      

  2. 异常的抛出

    使用throw关键字抛出异常:

    public static void main(String[] args) {
        int[] arr = {1,2,3};
        get(arr, 10);
    }
    
    public static int get(int[] arr, int index) {
        if (null == arr) {
            throw new NullPointerException("传递的数组为null");
        }
        if (index < 0 || index >= arr.length) {
            throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
        }
        return arr[index];
    }
    //执行结果
    //Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 传递的数组下标越界
    

    注意:

    1. throw必须写在方法体内;
    2. 抛出的对象必须是Exception或Exception的子类对象
    3. 如果抛出的是RunTimeException或者RunTimeException的子类,则可以不用处理,直接交给JVM来处理
    4. 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
    5. 异常一旦抛出,其后的代码就不会执行
  3. 异常的捕获

    主要有两种方式:异常声明throws 以及 try-catch捕获处理。

    • 异常声明throws

      public class ThrowsDemo {
          public static void riskyBusiness() throws Exception {
              throw new Exception("丢掉错误");
          }
      
          public static void main(String[] args) {
              try {
                  riskyBusiness();
              } catch (Exception e) {
                  System.out.println("主方法收拾烂摊子: " + e.getMessage());
              }
          }
      }
      

    • try-catch捕获并处理                                                                                                       

      try{   // 将可能出现异常的代码放在这里 }

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

       // 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类 时,就会被捕获到      

      // 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码 }

      [catch(异常类型 e){     // 对异常进行处理 }

      finally{      

       // 此处代码一定会被执行到

      }]

       // 后序代码

       // 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行

      // 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行

      示例:

      public static void main(String[] args) {
          try {
              System.out.println(10/0);
          } catch (ArithmeticException e) {
              System.out.println("出错了:" + e.getMessage());
              //这里可以进行错误处理,比如记录日志、恢复状态等
          } finally {
              System.out.println("不管如何,这块总会执行");
          }
          System.out.println("程序继续执行...");
      }
      

    注意:

    1. try块内抛出异常位置之后的代码将不会被执行
    2. 如果抛出异常类型与catch时异常类型不匹配,异常不会被成功捕获
    3. try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获

    多个异常的处理方式:

    int[] arr = {1, 2, 3}; 
    try {
        System.out.println("before");          
        // arr = null;            
        System.out.println(arr[100]);            
        System.out.println("after");
    } catch (ArrayIndexOutOfBoundsException e) {            
        System.out.println("这是个数组下标越界异常");
        e.printStackTrace();
    } catch (NullPointerException e) {            
        System.out.println("这是个空指针异常");            
        e.printStackTrace();
    }       
    System.out.println("after try catch");
    

    如果多个异常的处理方式是完全相同,也可以写成:

    catch (ArrayIndexOutOfBoundsException | NullPointerException e) { ... }
    

    如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误。

    int[] arr = {1, 2, 3};    
    try {        System.out.println("before"); 
           arr = null; 
           System.out.println(arr[100]); 
           System.out.println("after");  
     } 
    catch (Exception e)
     {   // Exception可以捕获到所有异常        e.printStackTrace();   }
    catch (NullPointerException e){  // 永远都捕获执行到        e.printStackTrace();   }   
         System.out.println("after try catch"); 
    

  4. Finally

    finally块中的代码无论是否发生异常,都会被执行。通常用于资源清理的工作。

    try {
        // 可能会发生异常的代码
    } catch (异常类型 e) {
        // 对捕获到的异常进行处理
    } finally {
        // 此处的语句无论是否发生异常,都会被执行到
    }
    

    注意:finally中的return会覆盖try或catch块中的return,但这是一种不好的编程习惯,应该避免。

  5. 异常的执行流程

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

三、自定义异常类

  1. 创建自定义异常类

    public class InvalidAgeException extends Exception {
        public InvalidAgeException(String message) {
            super(message);
        }
    }
    

  2. 使用自定义异常

    public Person(String name, int age) throws InvalidAgeException {
        this.name = name;
        if (age < 0 || age > 150) {
            throw new InvalidAgeException("年龄必须在0到150之间");
        }
        this.age = age;
    }
    

  3. 处理自定义异常

    try {
        Person person1 = new Person("Alice", 25);
        System.out.println("Person 1 created successfully.");
    
        Person person2 = new Person("Bob", -5);
    } catch (InvalidAgeException e) {
        System.out.println("Error: " + e.getMessage());
    }
    

完整示例:

// 自定义异常类
public class InvalidAgeException extends Exception {
    public InvalidAgeException(String message) {
        super(message);
    }
}

// 使用自定义异常的类
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) throws InvalidAgeException {
        this.name = name;
        if (age < 0 || age > 150) {
            throw new InvalidAgeException("年龄必须在0到150之间");
        }
        this.age = age;
    }

    public static void main(String[] args) {
        try {
            Person person1 = new Person("Alice", 25);
            System.out.println("Person 1 created successfully.");

            Person person2 = new Person("Bob", -5);
        } catch (InvalidAgeException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

四、异常的深入探讨

  1. Checked异常Unchecked异常的区别及使用场景

    • Checked异常:

      • 继承自Exception类,但不包括RuntimeException及其子类
      • 编译器强制要求处理这类异常
      • 通常用于可预见的、可恢复的错误情况
      • 例如:IOException, SQLException
      • 使用场景:                                                                                                             当你希望调用者必须意识到并处理可能的异常情况时                                         对于重要的业务逻辑,需要确保异常得到适当处理                                                 在API设计中,用于强制客户端处理某些错误情况
    • Unchecked异常:

      • 继承自RuntimeException
      • 编译器不强制要求处理
      • 通常用于程序错误或不可恢复的状态
      • 例如:NullPointerException, IllegalArgumentException
      • 使用场景:                                                                                                            对于程序逻辑错误,通常不应该被捕获,而应该修复代码.                                      当异常是由于编程错误导致,而非外部因素.                                                        在不希望强制调用者处理异常的情况下
  2. 选择使用检查型异常还是非检查型异常的最佳实践:

    • 如果异常是可恢复的,并且你希望强制调用者处理它,使用checked异常
    • 如果异常表示编程错误,使用unchecked异常
    • 如果不确定调用者是否能够恢复from这个异常,倾向于使用unchecked异常
    • 对于公共API,使用checked异常可以清晰地表达方法的契约
    • 在应用程序内部,更多地使用unchecked异常以减少代码中的异常处理逻辑
    • 避免过度使用checked异常,因为它们会导致代码变得冗长且难以维护
    • 考虑创建自定义异常来更好地描述特定的错误情况
  3. 异常处理对程序性能的影响:

    异常处理本身会带来一定的性能开销,主要体现在以下几个方面:

    • 创建异常对象:创建异常对象比普通对象更昂贵,因为它涉及收集堆栈跟踪信息
    • 捕获异常:try-catch块会略微降低代码的执行速度,即使异常没有被抛出
    • 堆栈跟踪:当异常被抛出时,生成堆栈跟踪信息是一个相对昂贵的操作

    然而,在大多数情况下,异常处理的性能影响是可以接受的,尤其是与异常处理带来的程序健壮性相比。

  4. 性能优化建议:

    • 避免使用异常进行正常的程序流程控制
    • 在可能的情况下,先进行条件检查,而不是依赖于捕获异常。
    • 在捕获异常时,尽可能精确地指定异常类型,避免使用笼统的Exception。
    • 在频繁执行的代码中,特别注意异常处理的使用。
    • 考虑使用日志框架而不是直接打印堆栈跟踪,可以更灵活地控制日志级别和输出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值