啪啪打脸!领导说——try-catch要放在循环体外!

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

转载声明:转载请注明出处,本技术博客是本人原创文章

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 啪啪打脸!领导说——try-catch要放在循环体外!

**来源:Java中文社群     **

今天给大家带来的是关于 try-catch 应该放在循环体外,还是放在循环体内的文章,我们将从性能业务场景分析这两个方面来回答此问题。

很多人对 try-catch 有一定的误解,比如我们经常会把它(try-catch)和“低性能”直接画上等号,但对 try-catch 的本质(是什么)却缺少着最基础的了解,因此我们也会在本篇中对 try-catch 的本质进行相关的探索

啪啪打脸!领导说:try-catch要放在循环体外!

小贴士:我会尽量用代码和评测结果来证明问题,但由于本身认知的局限,如有不当之处,请读者朋友们在评论区指出。

性能评测

话不多说,我们直接来开始今天的测试,本文我们依旧使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)来进行测试。

首先在 pom.xml 文件中添加 JMH 框架,配置如下:


!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core --
dependency
   groupIdorg.openjdk.jmh/groupId
   artifactIdjmh-core/artifactId
   version{version}/version
/dependency

完整测试代码如下:


import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * try - catch 性能测试
 */
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 1 轮,每次 1s
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Benchmark)
@Threads(100)
public class TryCatchPerformanceTest {
    private static final int forSize = 1000; // 循环次数
    public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(TryCatchPerformanceTest.class.getSimpleName()) // 要导入的测试类
                .build();
        new Runner(opt).run(); // 执行测试
    }

    @Benchmark
    public int innerForeach() {
        int count = 0;
        for (int i = 0; i  forSize; i++) {
            try {
                if (i == forSize) {
                    throw new Exception("new Exception");
                }
                count++;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return count;
    }

    @Benchmark
    public int outerForeach() {
        int count = 0;
        try {
            for (int i = 0; i  forSize; i++) {
                if (i == forSize) {
                    throw new Exception("new Exception");
                }
                count++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return count;
    }
}

以上代码的测试结果为:

从以上结果可以看出,程序在循环 1000 次的情况下,单次平均执行时间为:

  • 循环内包含 try-catch 的平均执行时间是 635 纳秒 ±75 纳秒,也就是 635 纳秒上下误差是 75 纳秒;
  • 循环外包含 try-catch 的平均执行时间是 630 纳秒,上下误差 38 纳秒。
  • 也就是说,在没有发生异常的情况下,除去误差值,我们得到的结论是:try-catch 无论是在 for 循环内还是   for 循环外,它们的性能相同,几乎没有任何差别

    啪啪打脸!领导说:try-catch要放在循环体外!

    try-catch的本质

    要理解 try-catch 的性能问题,必须从它的字节码开始分析,只有这样我能才能知道 try-catch 的本质到底是什么,以及它是如何执行的。

    此时我们写一个最简单的 try-catch 代码:

    
    public class AppTest {
        public static void main(String[] args) {
            try {
                int count = 0;
                throw new Exception("new Exception");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    然后使用 javac 生成字节码之后,再使用 javap -c AppTest 的命令来查看字节码文件:

    
    ➜ javap -c AppTest 
    警告: 二进制文件AppTest包含com.example.AppTest
    Compiled from "AppTest.java"
    public class com.example.AppTest {
      public com.example.AppTest();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."init":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: iconst_0
           1: istore_1
           2: new           #2                  // class java/lang/Exception
           5: dup
           6: ldc           #3                  // String new Exception
           8: invokespecial #4                  // Method java/lang/Exception."init":(Ljava/lang/String;)V
          11: athrow
          12: astore_1
          13: aload_1
          14: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
          17: return
        Exception table:
           from    to  target type
               0    12    12   Class java/lang/Exception
    }
    

    从以上字节码中可以看到有一个异常表:

    
    Exception table:
           from    to  target type
              0    12    12   Class java/lang/Exception
    

    参数说明:

  • from:表示 try-catch 的开始地址;
  • to:表示 try-catch 的结束地址;
  • target:表示异常的处理起始位;
  • type:表示异常类名称。
  • 从字节码指令可以看出,当代码运行时出错时,会先判断出错数据是否在 from 到 to 的范围内,如果是则从 target 标志位往下执行,如果没有出错,直接 goto 到 return。也就是说,如果代码不出错的话,性能几乎是不受影响的,和正常的代码的执行逻辑是一样的。

    啪啪打脸!领导说:try-catch要放在循环体外!

    业务情况分析

    虽然 try-catch 在循环体内还是循环体外的性能是类似的,但是它们所代码的业务含义却完全不同,例如以下代码:

    
    public class AppTest {
        public static void main(String[] args) {
            System.out.println("循环内的执行结果:" + innerForeach());
            System.out.println("循环外的执行结果:" + outerForeach());
        }
        
        // 方法一
        public static int innerForeach() {
            int count = 0;
            for (int i = 0; i  6; i++) {
                try {
                    if (i == 3) {
                        throw new Exception("new Exception");
                    }
                    count++;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return count;
        }
    
        // 方法二
        public static int outerForeach() {
            int count = 0;
            try {
                for (int i = 0; i  6; i++) {
                    if (i == 3) {
                        throw new Exception("new Exception");
                    }
                    count++;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return count;
        }
    }
    

    以上程序的执行结果为:

    java.lang.Exception: new Exception
    at com.example.AppTest.innerForeach(AppTest.java:15)
    at com.example.AppTest.main(AppTest.java:5)
    java.lang.Exception: new Exception
    at com.example.AppTest.outerForeach(AppTest.java:31)
    at com.example.AppTest.main(AppTest.java:6)
    循环内的执行结果:5
    循环外的执行结果:3

    at com.example.AppTest.innerForeach(AppTest.java:15)

    java.lang.Exception: new Exception

    at com.example.AppTest.main(AppTest.java:6)

    循环外的执行结果:3

    可以看出在循环体内的 try-catch 在发生异常之后,可以继续执行循环;而循环外的 try-catch 在发生异常之后会终止循环。

    因此我们在决定 try-catch 究竟是应该放在循环内还是循环外,不取决于性能(因为性能几乎相同),而是应该取决于具体的业务场景

    例如我们需要处理一批数据,而无论这组数据中有哪一个数据有问题,都不能影响其他组的正常执行,此时我们可以把 try-catch 放置在循环体内;而当我们需要计算一组数据的合计值时,只要有一组数据有误,我们就需要终止执行,并抛出异常,此时我们需要将 try-catch 放置在循环体外来执行。

    啪啪打脸!领导说:try-catch要放在循环体外!

    总结

    本文我们测试了 try-catch 放在循环体内和循环体外的性能,发现二者在循环很多次的情况下性能几乎是一致的。然后我们通过字节码分析,发现只有当发生异常时,才会对比异常表进行异常处理,而正常情况下则可以忽略 try-catch 的执行。但在循环体内还是循环体外使用 try-catch,对于程序的执行结果来说是完全不同的,因此我们应该从实际的业务出发,来决定到 try-catch 应该存放的位置,而非性能考虑

    琐碎时间想看一些技术文章,可以去公众号菜单栏翻一翻我分类好的内容,应该对部分童鞋有帮助。同时看的过程中发现问题欢迎留言指出,不胜感谢~。另外,有想多了解哪些方面内容的可以留言(什么时候,哪篇文章下留言都行),附菜单栏截图(PS:很多人不知道公众号菜单栏是什么)

    啪啪打脸!领导说:try-catch要放在循环体外!

    END

    啪啪打脸!领导说:try-catch要放在循环体外!

    我知道你 “在看啪啪打脸!领导说:try-catch要放在循环体外!

    原文始发于微信公众号(Java知音):啪啪打脸!领导说:try-catch要放在循环体外!

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    转载声明:转载请注明出处,本技术博客是本人原创文章

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 啪啪打脸!领导说——try-catch要放在循环体外!


     上一篇
    还在用单机版?教你用Docker+Redis搭建主从复制多实例 还在用单机版?教你用Docker+Redis搭建主从复制多实例
    0. 目标本地搭建三个redis实例(一主两备),实现效果:主实例插入数据备实例可以复制同步过去。 1. 安装和运行dockerdocker安装步骤省略,大家可以从官网下载并安装。 检查docker是否运行成功: docker info
    下一篇 
    Stream原理解析 Stream原理解析
    1. Stream的优势 图中4种stream接口继承自 BaseStream,其中 IntStream, LongStream, DoubleStream对应三种基本类型( int, long, double,注意不是包装类型)