如何消除Kotlin生成的不必要空检查以满足Jacoco分支覆盖率?
问题背景
你在使用Kotlin 1.3.50和Jacoco 0.8.4时遇到了一个棘手的问题:启用分支覆盖规则后,一段极简代码的测试无法通过,提示someWrapper调用行有1个分支未覆盖,而且无法排除所有调用someWrapper的类。
原Kotlin代码如下:
fun <U> someWrapper(supplier: () -> U): () -> (U) { return { supplier() } } fun foo(taskExecutor: TaskExecutor): Int { val future = CompletableFuture.supplyAsync(someWrapper { 42 }, taskExecutor::execute) return future.join() } @Test public void shouldFoo() { assertThat(foo(), is(42)); }
反编译Java代码后发现,问题出在编译器自动生成的if (var10000 != null)分支上——这个分支被IDE标记为“始终为true”,完全是多余的,但Jacoco依然把它算作未覆盖的分支:
public final int foo(TaskExecutor taskExecutor) { Object var10000 = WrappersKt.someWrapper((Function0)null.INSTANCE); if (var10000 != null) { Object var2 = var10000; var10000 = new Foo$sam$java_util_function_Supplier$0((Function0)var2); } Supplier var3 = (Supplier)var10000; // 其余代码省略... }
根本解决方案:调整代码消除多余空检查
这个空检查是Kotlin编译器在进行SAM转换时生成的——因为你把Kotlin的() -> U函数类型传给了需要JavaSupplier接口的supplyAsync方法,编译器自动生成了包装逻辑,并且为了兼容潜在的null值(尽管你的someWrapper不可能返回null),添加了这个防御性检查。
我们可以通过两种方式让编译器跳过这个多余的空检查:
方案1:让someWrapper直接返回Java的Supplier接口
直接修改someWrapper的返回类型为Java的Supplier,避免后续的SAM转换:
import java.util.function.Supplier fun <U> someWrapper(supplier: () -> U): Supplier<U> { return Supplier { supplier() } } fun foo(taskExecutor: TaskExecutor): Int { val future = CompletableFuture.supplyAsync(someWrapper { 42 }, taskExecutor::execute) return future.join() }
这样编译器不需要再做Kotlin函数到Java接口的转换,自然不会生成那个空检查分支。
方案2:显式转换为JavaSupplier
如果不想修改someWrapper的返回类型,可以在调用supplyAsync前显式把Kotlin函数转换成JavaSupplier:
import java.util.function.Supplier fun <U> someWrapper(supplier: () -> U): () -> (U) { return { supplier() } } fun foo(taskExecutor: TaskExecutor): Int { val kotlinSupplier = someWrapper { 42 } // 显式创建Java Supplier包装类 val javaSupplier = Supplier { kotlinSupplier() } val future = CompletableFuture.supplyAsync(javaSupplier, taskExecutor::execute) return future.join() }
这种方式也能绕过编译器自动生成的带空检查的SAM转换逻辑,Jacoco就能正常识别全分支覆盖了。
临时规避方案(已验证可行)
你提到的将supplyAsync调用提取到工具类并排除该类的方案确实可以快速解决问题:
fun <U> supplyAsync(supplier: () -> U, executor: TaskExecutor): CompletableFuture<U> { return CompletableFuture.supplyAsync(someWrapper { supplier() }, executor::execute) }
这种方式把问题隔离到一个单独的工具类中,只需要排除这个类的Jacoco检查即可,适合不想改动核心业务代码的场景。
为什么编译器会生成这个“不必要”的空检查?
在Kotlin 1.3.x版本中,编译器对SAM转换的空值处理逻辑比较保守:即使你的someWrapper逻辑上不可能返回null,编译器也无法静态推断出这一点(或者说没有做这个优化),因此生成了防御性的空检查分支来兼容所有可能的输入情况。后续的Kotlin版本(比如1.4及以上)对这个逻辑做了优化,会减少这类不必要的空检查生成。
内容的提问来源于stack exchange,提问作者BKE

