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

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

[[346237]]

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

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

性能评测

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

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

  1. <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->  
  2. <dependency>  
  3.    <groupId>org.openjdk.jmh</groupId>  
  4.    <artifactId>jmh-core</artifactId>  
  5.    <version>{version}</version>  
  6. </dependency> 

完整测试代码如下: 

  1. import org.openjdk.jmh.annotations.*;  
  2. import org.openjdk.jmh.runner.Runner;  
  3. import org.openjdk.jmh.runner.RunnerException;  
  4. import org.openjdk.jmh.runner.options.Options;  
  5. import org.openjdk.jmh.runner.options.OptionsBuilder;  
  6. import java.util.concurrent.TimeUnit;  
  7. /**  
  8.  * try - catch 性能测试  
  9.  */  
  10. @BenchmarkMode(Mode.AverageTime) // 测试完成时间  
  11. @OutputTimeUnit(TimeUnit.NANOSECONDS)  
  12. @Warmup(iterations = 1time = 1timeUnit = TimeUnit.SECONDS) // 预热 1 轮,每次 1s  
  13. @Measurement(iterations = 5time = 5timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s  
  14. @Fork(1) // fork 1 个线程  
  15. @State(Scope.Benchmark)  
  16. @Threads(100) 
  17. public class TryCatchPerformanceTest {  
  18.     private static final int forSize = 1000; // 循环次数  
  19.     public static void main(String[] args) throws RunnerException {  
  20.         // 启动基准测试  
  21.         Options opt = new OptionsBuilder()  
  22.                 .include(TryCatchPerformanceTest.class.getSimpleName()) // 要导入的测试类  
  23.                 .build();  
  24.         new Runner(opt).run(); // 执行测试  
  25.     }  
  26.     @Benchmark  
  27.     public int innerForeach() {  
  28.         int count = 0 
  29.         for (int i = 0; i < forSize; i++) {  
  30.             try {  
  31.                 if (i == forSize) {  
  32.                     throw new Exception("new Exception");  
  33.                 }  
  34.                 count++;  
  35.             } catch (Exception e) {  
  36.                 e.printStackTrace(); 
  37.             }  
  38.         }  
  39.         return count;  
  40.     }  
  41.     @Benchmark  
  42.     public int outerForeach() {  
  43.         int count = 0 
  44.         try {  
  45.             for (int i = 0; i < forSize; i++) {  
  46.                 if (i == forSize) {  
  47.                     throw new Exception("new Exception");  
  48.                 }  
  49.                 count++;  
  50.             }  
  51.         } catch (Exception e) {  
  52.             e.printStackTrace();  
  53.         }  
  54.         return count;  
  55.     }  

以上代码的测试结果为:

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

  •  循环内包含 try-catch 的平均执行时间是 635 纳秒 ±75 纳秒,也就是 635 纳秒上下误差是 75 纳秒;
  •  循环外包含 try-catch 的平均执行时间是 630 纳秒,上下误差 38 纳秒。

也就是说,在没有发生异常的情况下,除去误差值,我们得到的结论是:try-catch 无论是在 for 循环内还是  for 循环外,它们的性能相同,几乎没有任何差别。

try-catch的本质

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

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

  1. public class AppTest {  
  2.     public static void main(String[] args) {  
  3.         try {  
  4.             int count = 0 
  5.             throw new Exception("new Exception"); 
  6.         } catch (Exception e) {  
  7.             e.printStackTrace();  
  8.         }  
  9.     }  

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

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

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

  1. Exception table:  
  2.        from    to  target type  
  3.           0    12    12   Class java/lang/Exception 

参数说明:

  •  from:表示 try-catch 的开始地址;
  •  to:表示 try-catch 的结束地址;
  •  target:表示异常的处理起始位;
  •  type:表示异常类名称。

从字节码指令可以看出,当代码运行时出错时,会先判断出错数据是否在 from 到 to 的范围内,如果是则从 target 标志位往下执行,如果没有出错,直接 goto 到 return。也就是说,如果代码不出错的话,性能几乎是不受影响的,和正常的代码的执行逻辑是一样的。

业务情况分析

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

  1. public class AppTest {  
  2.     public static void main(String[] args) {  
  3.         System.out.println("循环内的执行结果:" + innerForeach());  
  4.         System.out.println("循环外的执行结果:" + outerForeach());  
  5.     }    
  6.     // 方法一  
  7.     public static int innerForeach() {  
  8.         int count = 0 
  9.         for (int i = 0; i < 6; i++) {  
  10.             try {  
  11.                 if (i == 3) {  
  12.                     throw new Exception("new Exception");  
  13.                 } 
  14.                 count++;  
  15.             } catch (Exception e) {  
  16.                 e.printStackTrace(); 
  17.             } 
  18.         }  
  19.         return count;  
  20.     }  
  21.     // 方法二  
  22.     public static int outerForeach() {  
  23.         int count = 0 
  24.         try {  
  25.             for (int i = 0; i < 6; i++) {  
  26.                 if (i == 3) {  
  27.                     throw new Exception("new Exception");  
  28.                 }  
  29.                 count++;  
  30.             }  
  31.         } catch (Exception e) {  
  32.             e.printStackTrace();  
  33.         }  
  34.         return count;  
  35.     }  

以上程序的执行结果为:

 

  1. java.lang.Exception: new Exception 
  2.  
  3. at com.example.AppTest.innerForeach(AppTest.java:15) 
  4.  
  5. at com.example.AppTest.main(AppTest.java:5) 
  6.  
  7. java.lang.Exception: new Exception 
  8.  
  9. at com.example.AppTest.outerForeach(AppTest.java:31) 
  10.  
  11. at com.example.AppTest.main(AppTest.java:6) 
  12.  
  13. 循环内的执行结果:5 
  14.  
  15. 循环外的执行结果:3 

 

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

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

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

总结

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

 

责任编辑:庞桂玉 来源: Java知音
相关推荐

2020-10-29 07:07:38

循环体外Java

2009-07-21 14:30:38

Scalatry-catch

2022-02-17 13:17:06

网络安全数据泄露漏洞

2020-05-29 08:14:49

代码Try-Catch程序员

2017-11-02 15:26:10

JavaScriptasync错误

2022-01-25 12:14:39

面试try-catch代码

2019-08-20 09:23:15

Apple PenciiPhone苹果

2020-08-24 13:35:59

trycatchJava

2020-09-27 07:48:40

不用try catch

2009-12-02 19:56:33

PHP中try{}ca

2023-11-13 17:01:26

C++编程

2023-09-07 07:53:21

JavaScriptGoRust

2021-03-31 11:52:24

try-catch-fJava代码

2023-05-26 15:10:46

2020-06-15 08:12:51

try catch代码处理器

2023-05-16 15:32:45

JavaScriptWeb前端工程师

2021-01-13 09:55:29

try-catch-fJava代码

2023-01-31 09:49:36

硅谷裁员

2012-10-24 13:53:25

2020-06-28 09:08:08

Java语法块开发
点赞
收藏

51CTO技术栈公众号