JavaEE之异常与自定义异常

9 阅读9分钟

认识异常

什么是异常?

  • 异常代表程序出现的问题,如下方代码
int[] arr = {10, 20, 30};
System.out.println(arr[3]);
  • 程序运行报错如下

  • 例如:如下代码

System.out.println(10 / 0);
  • 程序运行报错如下

  • 还有其他问题:

    • 读取的文件不存在了

    • 读取网络数据,断网了

Java异常体系

  • 异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception,平常所说的异常指java.lang.Exception,而因为断网断电导致的不可处理的异常指Error

    image.png 在开发中经常提到的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。

  • 可以通过查看api树的方式来查看异常类,和其子类的详细说明Class Hierarchy (Java SE 17 & JDK 17)

image.png

  • Exception:叫异常,它代表的才是我们程序可能出现的问题,所以,我们程序员通常会用Exception以及它的子类来封装程序出现的问题
    • 运行时异常: RuntimeException及其子类,编译阶段不会出现错误提醒,运行时出现的异常(如:数组索引越界异常)
    • 编译时异常:编译阶段就会出现错误提醒的。(如:日期解析异常)
     /**
    * @Description Demo011
    * @Author songyu
    * @Date 2025-09-30  8:54
    */
    public class Demo011 {
        public static void main(String[] args) {
            //运行时异常(代码编译完成后运行的时候出现异常)
            int a = 1/0;
            //编译时异常(在写代码,和代码编译阶段就会出现异常)
            InputStream in = new FileInputStream (  "d:/demo.txt"  ); 
        }
    }
    
    我们在调用new FileInputStream("d:/demo.txt")方法时,可能磁盘并没有这个文件所以这里可能会出现异常。
  • Java比较贴心,它为了更加强烈的提醒方法的调用者,设计了编译时异常,它把异常的提醒提前了,只要可能有问题就给你报出异常提示(代码上的红色波浪线报错)。
  • 运行时异常特点:
    • 在运行时在控制台弹出异常信息并停止JVM运行
  • 编译时异常的特点:
    • 编译阶段有警告提示,需要我们进行手动处理;
    • 另外与与运行期异常一样的是,运行中出现的非正常的情况,最终会导致JVM的非正常停止。

处理异常

抛出异常(throws)
  • 在方法上使用throws关键字,可以将方法内部出现的异常抛出去给调用者处理。
    方法 throws 异常1 ,异常2 ,异常3 ..{
           …
     }
    

捕获异常(try…catch)

  • 直接捕获程序出现的异常。
    try{
        // 监视可能出现异常的代码! 
    }catch(异常类型1 变量){
        // 处理异常
    }catch(异常类型2 变量){
        // 处理异常
    }...
    
  • 企业实践常见异常处理方案:底层异常层层往上抛出,最外层捕获异常(捕获异常时最后要捕获父类父类异常,用于兜底),记录下异常信息,并响应适合用户观看的信息进行提示,同时因为在最外层捕获了异常,所以异常不终程序运行,提升了用户体验 image.png
  • 实例代码:
public class Demo012 {
    public static void main(String[] args) {
        //这里main就是最外面的方法,所以这里推荐处理异常进行try-catch捕获使用方案1
        //调用demo()
        try {
            demo();
        }catch (FileNotFoundException e){
            e.printStackTrace(); //打印异常堆栈信息给程序员看的
            System.out.println("您访问的资源不存在,请重新操作!"); 
            //友好提示信息给用户看的(以后给前端)
        }catch(Exception e){ 
        //兜底异常处理,不知道可能会发生什么异常,这里使用父类可以捕获任何异常返回友好信息
        e.printStackTrace();
            System.out.println("服务器忙,请稍重试。。。");
        }
    }

    //demo不是最外层,所以推荐将异常抛出去,使用方案2处理
    public static void demo()throws FileNotFoundException {
        //运行时异常
        int a = 1/0;
        //编译时异常
        InputStream in = new FileInputStream("d:/demo.txt");
    }
}

异常处理的注意

  • 注意1:当抛出子类异常非常多,可以直接抛出一个父类异常简化代码
    方法 throws 子类异常1,子类异常2...{}
    
    优化为如下
    
    方法 throws Exception{}
    
  • 注意2:catch(异常类型) 子类异常在上,父类异常在下,因为优先匹配子类明确的异常,不明确由父类兜底,如父类Exception
    try{
    
    监视可能出现异常的代
    
    }catch(子类异常类型 变量){
    
    //1处理异常
    
    }catch(父类异常类型 变量){
    
    //2处理异常
    
    }
    

异常的作用

  • 作用1:异常是用来定位程序bug的关键信息

  • 作用2:可以作为方法内部的一种特殊返回值(主动抛出异常),以便通知上层调用者,方法的执行问题

    方法(){
          throw new RuntimeException();
     }
    

    如下方案例所示:

     /**
    * @Description Demo013
    * @Author songyu
    * @Date 2025-09-30  9:47
    */
    public class Demo013 {
        public static void main(String[] args) {
            //目标:控制台输入一个性别,如果是男结束操作,抛出提示只允许女性进入
            // 代码中抛出异常作为特殊的方法返回值,语法: throw 异常对象
            //1.创建Scanner对象
            Scanner sc = new Scanner(System.in);
            //2.提示用户输入性别
            System.out.println("请输入您的性别:");
            String gender = sc.next();
            //3.得到性别,判断是否是男,如果是男抛出异常结束程序(作为特殊的方法返回值)
            if("男".equals(gender)){
                throw new RuntimeException("男性禁止入内");
                //抛出异常就是结束程序,不会往下走
            }
            //4.女性允许进入
            System.out.println("女性允许进入。。。");
        }
    }
    

小结

  1. 异常是什么?
    • 异常是代码在编译或者执行的过程中可能出现的错误。
  2. 异常的代表是谁?分为几类?
    • Exception,分为两类:编译时异常、运行时异常。
    • 编译时异常:没有继承RuntimeExcpetion的异常,编译阶段就会出错。
    • 运行时异常:继承自RuntimeException的异常或其子类,编译阶段不报错,运行时出现的。
  3. 异常的作用是啥?
    • 用来查找bug;可以作为方法内部的特殊返回值,通知上层调用者底层的执行情况。
  4. 异常处理的常见方案是什么样的?
    • 在开发中异常的常见处理方式是:底层的异常抛出去给最外层,最外层集中捕获处理。

自定义异常

为什么需要自定义异常

Java无法为这个世界上全部的问题都提供异常类来代表,如果企业自己的某种问题想通过异常来表示,以便用异常来管理该问题,那就需要自己来定义异常类了。

image.png

自定义异常类型两种方案

  • 运行时异常
    • 定义一个异常类继承RuntimeException
    • 重写构造函数
    • 通过throw new 异常类(xxx)来创建异常类对象并抛出
    • 特点:编译阶段不报错,运行时才可能出现!提醒不属于激进型
  • 自定义编译时异常
    • 定义一个异常类型Exception
    • 重写构造函数
    • 通过throw new 异常类(xxx)创建异常对象并抛出
    • 特点:编译阶段就报错,提醒比较激进

实际场景演示

需求分析
  • 需求:写一个saveAge(int age)方法,在方法中对参数age进行判断,如果age小于0或者大于等于200就认为年龄不合法,如果年龄不合法就抛出一个年龄非法异常给调用者
  • 分析:Java的API中没有年龄非法异常这个异常,所以我们需要自定义一个异常类,用来表示年龄非法异常,然后在方法中抛出异常即可。
代码实现
  • 自定义运行时异常实现代码
    package AgeException;
    //定义一个运行时异常类继承RuntimeException.
    public class AgeRunTimeException extends RuntimeException {
        // 重写构造器
        public AgeRunTimeException() {
        }
        // 有参构造器
        public AgeRunTimeException(String message) {
            super(message);
            //将消息给到父类的成员,便于以后调用父类的getMessage()再次获取异常消息
        }
    }
    
  • 自定义编译时异常实现代码
    package AgeException;
    public class AgeException extends RuntimeException {
        //重写构造器
        public AgeException() {
        }
        // 有参构造器
        public AgeException(String message) {
            //将消息给到父类的成员,便于以后调用父类的getMessage()再次获取异常消息
            super(message);
        }
    }
    
  • 在设计的函数方法中,指定发生年龄异常后抛出异常
    package AgeException;
    
    import java.util.Scanner;
    
    public class compareAge {
        public static void compareAge(int age) throws  AgeRunTimeException {
            if (age <= 0 || age > 200) {
                throw new AgeRunTimeException("年龄错误");
            } else {
                System.out.println("您的年龄是:" + age);
            }
        }
    
        public static void compareAge2(int age) throws  AgeException {
            if (age < 0 || age > 200) {
                throw new AgeException("年龄错误");
            } else {
                System.out.println("您的年龄是:" + age);
            }
        }
        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入您的年龄:");
            int age = scanner.nextInt();
            try {
                compareAge(age);
            } catch (AgeRunTimeException e) {
                System.out.println(e.getMessage());
            } catch (Exception e){
                System.out.println("服务器忙,请稍后重试!");
                e.printStackTrace();
            }
        }
    }
    
  • 可以看到自定义编译时异常,每一步都要处理 image.png
  • 自定义运行时异常则可以不在代码中处理,但是不处理对用户的时候不友好 image.png 注意事项:自定义异常可能是编译时异常,也可以是运行时异常。
  1. 如果自定义异常类继承Excpetion,则是编译时异常。
    • 特点:方法中抛出的是编译时异常,必须在方法上使用throws声明,强制调用者处理。
  2. 如果自定义异常类继承RuntimeException,则运行时异常。
    • 特点:方法中抛出的是运行时异常,不需要在方法上用throws声明。

异常的处理方案

  • 方案1:底层异常层层往上抛出,最外层捕获异常,记录下异常信息,并响应适合用户观看的信息进行提示,如我们之前的例子所示 image.png
  • 方案2:最外层捕获异常后,尝试重新修复,如下方例子所示
    • 需求:控制台输入价格,价格不合法可以处理异常并恢复程序重新输入价格,直到输入合法的价格
    • 实现:在捕获异常后恢复程序,如下方代码所示:
    import java.util.Scanner;
    public class DemoPrice {
        public static void main(String[] args) {
            getPrice();
        }
    
    
        public static double getPrice() {
      //控制台输入价格,价格不合法可以处理异常并恢复程序重新输入价格,直到输入合法的价格
            //Scanner scanner = new Scanner(System.in);
            
            //Scanner对象不能再for循环外面,在外面下一次循环上次的参数还在scanner对象中,
            // 循环无法被键盘输入函数阻塞,循环会变为死循环
            double price = 0.0D;
            while (true) {
                try {
                    Scanner scanner = new Scanner(System.in);
                    // 输入提示词
                    System.out.println("请输入商品价格:");
                    // 获取价格
                    price = scanner.nextDouble();
                    // 合法的输入则允许接下来的程序,使用return跳出循环
                    System.out.println("商品价格是:" + price);
                    return price;
                } catch (Exception e) {
                    System.out.println("输入价格不合法,请重新输入");
                }
                }
            }
       }