BlazeMaple BlazeMaple
首页
  • 基础知识

    • Java的基本数据类型
    • Java中的常用类String
    • Java中的异常
    • Java中的注解
    • Java中的反射机制
    • Java中的泛型
    • Java为什么是值传递
  • 集合框架

    • Java集合核心知识总结
    • HashMap的7种遍历方式
    • 源码分析
  • Java新特性

    • Java8新特性
  • IO流

    • Java基础IO总结
    • Java IO中的设计模式
    • Java IO模型
    • IO多路复用详解
  • 并发编程

    • 并发编程基础总结
  • JVM

    • JVM基础总结
  • MySQL

    • MySQL核心知识小结
    • MySQL 45讲
  • Redis

    • Redis核心入门知识简记
  • Spring
  • SpringCloud Alibaba
  • 开发工具

    • Git详解
    • Maven详解
    • Docker详解
    • Linux常用命令
  • 在线工具

    • json (opens new window)
    • base64编解码 (opens new window)
    • 时间戳转换 (opens new window)
    • unicode转换 (opens new window)
    • 正则表达式 (opens new window)
    • md5加密 (opens new window)
    • 二维码 (opens new window)
    • 文本比对 (opens new window)
  • 学习资源

    • 计算机经典电子书PDF
    • hot120
GitHub (opens new window)
首页
  • 基础知识

    • Java的基本数据类型
    • Java中的常用类String
    • Java中的异常
    • Java中的注解
    • Java中的反射机制
    • Java中的泛型
    • Java为什么是值传递
  • 集合框架

    • Java集合核心知识总结
    • HashMap的7种遍历方式
    • 源码分析
  • Java新特性

    • Java8新特性
  • IO流

    • Java基础IO总结
    • Java IO中的设计模式
    • Java IO模型
    • IO多路复用详解
  • 并发编程

    • 并发编程基础总结
  • JVM

    • JVM基础总结
  • MySQL

    • MySQL核心知识小结
    • MySQL 45讲
  • Redis

    • Redis核心入门知识简记
  • Spring
  • SpringCloud Alibaba
  • 开发工具

    • Git详解
    • Maven详解
    • Docker详解
    • Linux常用命令
  • 在线工具

    • json (opens new window)
    • base64编解码 (opens new window)
    • 时间戳转换 (opens new window)
    • unicode转换 (opens new window)
    • 正则表达式 (opens new window)
    • md5加密 (opens new window)
    • 二维码 (opens new window)
    • 文本比对 (opens new window)
  • 学习资源

    • 计算机经典电子书PDF
    • hot120
GitHub (opens new window)
  • 基础知识

    • Java的基本数据类型
    • 聊一聊Java中的常用类String
    • 聊一聊Java中的异常
      • 聊一聊Java中的注解
      • 聊一聊Java中的反射机制
      • 聊一聊Java中的泛型
      • 聊一聊Java为什么是值传递
    • 集合框架

      • Java集合核心知识总结
      • HashMap 的 7 种遍历方式
      • 源码分析

        • ArrayList源码分析
        • LinkedList源码分析
        • HashMap源码分析
        • ConcurrentHashMap源码分析
        • CopyOnWriteArrayList 源码分析
        • LinkedHashMap 源码分析
        • ArrayBlockingQueue 源码分析
        • PriorityQueue 源码分析
        • DelayQueue 源码分析
    • Java新特性

      • Java8新特性
    • Java基础
    • 基础知识
    BlazeMaple
    2023-10-30
    目录

    聊一聊Java中的异常

    # 什么是异常?

    关于Java的异常,我们认为符合大致分为以下几种情况:

    1. 程序逻辑运行结果不符合预期。
    2. 程序执行时抛出各种exception。
    3. 因为各种原因导致服务崩溃。

    Java异常类体系结构如下图所示:

    image-20231030233619616

    # Exception 和 Error 有什么区别?

    Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。

    • 受检异常:受检异常的方法在调用时需要抛出的潜在风险进行处理,要么捕获,要么向上抛。如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。
    • 不受检异常:非受检异常即运行时才可能知晓的异常,该异常可由开发人员结合业务场景确定是否捕获。Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。RuntimeException 及其子类都统称为非受检查异常。

    Error:Error 属于程序无法处理的错误 ,不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

    # throw与throws的区别

    它们唯一的区别就在定义的位置:

    1. throws放在函数上,throw放在函数内。
    2. throws可以跟多个错误类,throw只能跟单个异常对象

    # Throwable 类常用方法有哪些?

    • String getMessage(): 返回异常发生时的简要描述
    • String toString(): 返回异常发生时的详细信息
    • String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
    • void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息

    # Throwable 类常用方法有哪些?

    • String getMessage(): 返回异常发生时的简要描述
    • String toString(): 返回异常发生时的详细信息
    • String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
    • void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息

    # try-catch-finally 如何使用?

    • try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
    • catch块:用于处理 try 捕获到的异常。
    • finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

    代码示例:

    try {
        System.out.println("Try to do something");
        throw new RuntimeException("RuntimeException");
    } catch (Exception e) {
        System.out.println("Catch Exception -> " + e.getMessage());
    } finally {
        System.out.println("Finally");
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    输出:

    Try to do something
    Catch Exception -> RuntimeException
    Finally
    
    1
    2
    3

    注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。

    # finally 中的代码一定会执行吗?

    就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。

    try {
        System.out.println("Try to do something");
        throw new RuntimeException("RuntimeException");
    } catch (Exception e) {
        System.out.println("Catch Exception -> " + e.getMessage());
        // 终止当前正在运行的Java虚拟机
        System.exit(1);
    } finally {
        System.out.println("Finally");
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    输出:

    Try to do something
    Catch Exception -> RuntimeException
    
    1
    2

    另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:

    1. 程序所在的线程死亡。
    2. 关闭 CPU。

    # 如何使用 try-with-resources

    适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象

    关闭资源和 finally 块的执行顺序: 在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

    Java 中类似于InputStream、OutputStream、Scanner、PrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求:

    Scanner scanner = null;
    try {
        scanner = new Scanner(new File("D://read.txt"));
        while (scanner.hasNext()) {
            System.out.println(scanner.nextLine());
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (scanner != null) {
            scanner.close();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    使用 Java 7 之后的 try-with-resources 语句改造上面的代码:

    try (Scanner scanner = new Scanner(new File("test.txt"))) {
        while (scanner.hasNext()) {
            System.out.println(scanner.nextLine());
        }
    } catch (FileNotFoundException fnfe) {
        fnfe.printStackTrace();
    }
    
    1
    2
    3
    4
    5
    6
    7

    当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单,如果还是用try-catch-finally可能会带来很多问题。通过使用分号分隔,可以在try-with-resources块中声明多个资源。

    try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
         BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
        int b;
        while ((b = bin.read()) != -1) {
            bout.write(b);
        }
    }
    catch (IOException e) {
        e.printStackTrace();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    # 异常使用有哪些需要注意的地方?

    • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
    • 抛出的异常信息一定要有意义。
    • 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException。
    • 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)

    # 多异常捕获处理技巧

    对于多异常需要捕获处理时,我们建议符合以下三大原则:

    1. 有几个异常就处理几个异常,如果无法处理就抛出。
    2. 父类exception放在最下方。
    3. 多异常建议使用|进行归类整理。

    # 不要用异常控制流程

    不要使用try块语句控制业务执行流程,原因如下:

    1. try-catch阻止JVM试图进行的优化,所以当我们要使用try块时,使用的粒度尽可能要小一些。
    2. 现代标准遍历模式并不会导致冗余检查,所以我们无需为了避免越界检查而使用try块解决问题。

    错误示例,不仅可读性差,而且性能不佳。

    public static void stackPopByCatch() {
            long start = System.currentTimeMillis();
            try {
                //插入1000w个元素
                Stack stack = new Stack();
                for (int i = 0; i < 1000_0000; i++) {
                    stack.push(i);
                }
                //使用pop抛出的异常判断出栈是否结束
                while (true) {
                    try {
                        stack.pop();
                    } catch (Exception e) {
                        System.out.println("出栈结束");
                        break;
                    }
                }
            } catch (Exception e) {
    
            }
            long end = System.currentTimeMillis();
            System.out.println("使用try进行异常捕获,执行时间:" + (end - start));
    
    
            start = System.currentTimeMillis();
            Stack stack2 = new Stack();
            for (int i = 0; i < 1000_0000; i++) {
                stack2.push(i);
            }
            //使用for循环控制代码流程
            int size = stack2.size();
    
            for (int i = 0; i < size; i++) {
                stack2.pop();
            }
            end = System.currentTimeMillis();
            System.out.println("使用逻辑进行出栈操作,执行时间:" + (end - start));
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    输出结果,可以看到用try块控制业务流程性能很差:

    出栈结束
    使用try进行异常捕获,执行时间:2613
    使用逻辑进行出栈操作,执行时间:1481
    
    1
    2
    3

    # 避免频繁抛出和捕获异常

    如下代码所示,可以看到频繁抛出和捕获对象是非常耗时的,所以不建议使用异常来作为处理逻辑,可以和前端协商好错误码从而避免没必要的性能开销

    	private int testTimes;
        public ExceptionTest(int testTimes) {
            this.testTimes = testTimes;
        }
        public void newObject() {
            long l = System.currentTimeMillis();
            for (int i = 0; i < testTimes; i++) {
                new Object();
            }
            System.out.println("建立对象:" + (System.currentTimeMillis() - l));
        }
        public void newException() {
            long l = System.currentTimeMillis();
            for (int i = 0; i < testTimes; i++) {
                new Exception();
            }
            System.out.println("建立异常对象:" + (System.currentTimeMillis() - l));
        }
        public void catchException() {
            long l = System.currentTimeMillis();
            for (int i = 0; i < testTimes; i++) {
                try {
                    throw new Exception();
                } catch (Exception e) {
                }
            }
            System.out.println("建立、抛出并接住异常对象:" + (System.currentTimeMillis() - l));
        }
        public void catchObj() {
            long l = System.currentTimeMillis();
            for (int i = 0; i < testTimes; i++) {
                try {
                    new Object();
                } catch (Exception e) {
                }
            }
            System.out.println("建立,普通对象并catch:" + (System.currentTimeMillis() - l));
        }
    
        public static void main(String[] args) {
            ExceptionTest test = new ExceptionTest(100_0000);
            test.newObject();
            test.newException();
            test.catchException();
            test.catchObj();
         
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47

    输出结果:

    输出结果
    建立对象:3
    建立异常对象:484
    建立、抛出并接住异常对象:539
    建立,普通对象并catch:3
    
    1
    2
    3
    4
    5

    # 尽可能在for循环外捕获异常

    try-catch时会捕获并创建异常对象,所以如果在for循环内频繁捕获异常会创建大量的异常对象:

    	public static void main(String[] args) {
            outerTryCatch();
            innerTryCatch();
        }
    
    	//for 外部捕获异常
        private static void outerTryCatch() {
            long memory = Runtime.getRuntime().freeMemory();
            try {
                for (int i = 0; i < 30_0000; i++) {
                    int num = 10 / 0;
                    System.out.println("Fnum:" + num);
                }
            } catch (Exception e) {
            }
            long useMemory = (memory - Runtime.getRuntime().freeMemory()) / 1024 / 1024;
            System.out.println("cost memory:" + useMemory + "M");
        }
    
    	//for 内部捕获异常
        private static void innerTryCatch() {
            long memory = Runtime.getRuntime().freeMemory();
            for (int i = 0; i < 30_0000; i++) {
                try {
                    int num = 10 / 0;
                    System.out.println("num:" + num);
                } catch (Exception e) {
                }
            }
            long useMemory = (memory - Runtime.getRuntime().freeMemory()) / 1024 / 1024;
            System.out.println("cost memory:" + useMemory + "M");
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    输出结果如下,可以看到for循环内部捕获异常消耗了22M的堆内存。原因很简单,for外部捕获异常时,会直接终止for循环,而在for循环内部捕获异常仅结束本次循环的,所以如果for循环频繁报错,那么在内部捕获异常尽可能创建大量的异常对象。

    cost memory:0M
    cost memory:22M
    
    1
    2

    # 参考文献

    Java基础常见面试题总结(下):https://javaguide.cn/java/basis/java-basic-questions-03.html#项目相关(opens new window) (opens new window)

    Effective Java中文版(第3版):https://book.douban.com/subject/30412517/(opens new window) (opens new window)

    阿里巴巴Java开发手册:https://book.douban.com/subject/27605355/(opens new window) (opens new window)

    Java核心技术·卷 I(原书第11版):https://book.douban.com/subject/34898994/(opens new window) (opens new window)

    Java 基础 - 异常机制详解:https://www.pdai.tech/md/java/basic/java-basic-x-exception.html#异常是否耗时为什么会耗时

    帮助我们改善此页面! (opens new window)
    上次更新: 2024/08/13, 09:07:12
    聊一聊Java中的常用类String
    聊一聊Java中的注解

    ← 聊一聊Java中的常用类String 聊一聊Java中的注解→

    最近更新
    01
    SpringCloud Alibaba实战
    08-22
    02
    SpringCloud Alibaba核心知识
    08-22
    03
    两数之和
    08-08
    更多文章>
    Theme by Vdoing | Copyright © 2023-2024 BlazeMaple
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式