try-catch 应该放在循环体外,还是放在循环体内,很多人对此有一定的误解,比如我们经常会把它(try-catch)和“低性能”直接画上等号,但对 try-catch 的本质(是什么)却缺少着最基础的了解,因此我们从性能和业务场景分析这两个方面来分析此问题。
使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)来进行测试。
<!-- 添加JMH框架 --> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-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 代码:
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 在循环体内还是循环体外的性能是类似的,但是它们所代码的业务含义却完全不同,例如以下代码:
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
可以看出在循环体内的 try-catch 在发生异常之后,可以继续执行循环;而循环外的 try-catch 在发生异常之后会终止循环。
因此我们在决定 try-catch 究竟是应该放在循环内还是循环外,不取决于性能(因为性能几乎相同),而是应该取决于具体的业务场景。
例如我们需要处理一批数据,而无论这组数据中有哪一个数据有问题,都不能影响其他组的正常执行,此时我们可以把 try-catch 放置在循环体内;而当我们需要计算一组数据的合计值时,只要有一组数据有误,我们就需要终止执行,并抛出异常,此时我们需要将 try-catch 放置在循环体外来执行。
二者在循环很多次的情况下性能几乎是一致的。然后我们通过字节码分析,发现只有当发生异常时,才会对比异常表进行异常处理,而正常情况下则可以忽略 try-catch 的执行。但在循环体内还是循环体外使用 try-catch,对于程序的执行结果来说是完全不同的,因此我们应该从实际的业务出发,来决定到 try-catch 应该存放的位置,而非性能考虑。