kotlin使用let报java.lang.NoClassDefFoundError

问题阐述 如下代码:

private fun shareUrlToFriend(logoUrl: String) { activity?.let { Glide.with(this) .asBitmap() .load(logoUrl) .into(object : CustomTarget() { override fun onLoadCleared(placeholder: Drawable?) {}override fun onResourceReady(resource: Bitmap, transition: Transition?) { print(logoUrl)//就是一个方法使用了logoUrl} }) } }

运行这段代码报java.lang.NoClassDefFoundError错误(表示运行中找不到类的定义)。经过替换尝试,报错不是Glide的锅。根据kotlin默认最后一行是返回值的规则,这代码最后let下面最后一个返回对象是CustomTarget的匿名内部类对象。因为:
// Glide into()方法 @NonNull public > Y into(@NonNull Y target) { return into(target, /*targetListener=*/ null, Executors.mainThreadExecutor()); }

所以,可以把上面代码替换成如下简单代码:
class T { var a: Any? = null fun f(u: String) { a?.let { object : Inter { override fun e() { print(u) } } } }}fun main() { val t = T() t.a = Any() t.f("u") }interface Inter { fun e() }

这段执行f()函数代码会报一样的错误。
Exception in thread "main" java.lang.NoClassDefFoundError: com/a/wzm/shere/ui/T$f$1$1 at com.a.wzm.shere.ui.T.f(Test.kt:14) at com.a.wzm.shere.ui.TestKt.main(Test.kt:28) at com.a.wzm.shere.ui.TestKt.main(Test.kt) Caused by: java.lang.ClassNotFoundException: com.a.wzm.shere.ui.T$f$1$1 at java.net.URLClassLoader.findClass(URLClassLoader.java:382) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 3 moreProcess finished with exit code 1

T$f$1$1是个什么鬼?
问题追溯 查看翻译后的Java代码:
//NO.0主要看f()函数。 public final void f(@NotNull String u) { Intrinsics.checkParameterIsNotNull(u, "u"); if (this.a != null) { boolean var3 = false; boolean var4 = false; int var6 = false; 1 var10000 = (1)(new T$f$$inlined$let$lambda$1(u)); // 1是啥? }}

里面这个1就是不没定义的类型(也就是报错里面需要定义的T$f$1$1)。为什么有个1出来捣乱?
//生成的类。 @Metadata( mv = {1, 1, 15}, bv = {1, 0, 3}, k = 1, d1 = {"\u0000\u0011\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000*\u0001\u0000\b\n\u0018\u00002\u00020\u0001J\b\u0010\u0002\u001a\u00020\u0003H\u0016¨\u0006\u0004?\u0006\u0000"}, d2 = {"com/a/wzm/shere/ui/Te$f$1$1", "Lcom/a/wzm/share/ui/Inter; ", "a", "", "app"} ) public final class Te$f$$inlined$let$lambda$1 implements Inter { // $FF: synthetic field final String $u$inlined; Te$f$$inlined$let$lambda$1(String var1) { this.$u$inlined = var1; }public void a() { String var1 = this.$u$inlined; boolean var2 = false; System.out.print(var1); } }

类名跟注解里的("com/a/wzm/share/ui/Te$f$1$1")不一致。
实验 对上诉代码简单修改测试。发现只需要简单修改就不报错。比如:
  1. 不用 let第一行用if(a==null)return。(规避了let的问题,不讨论)。
  2. a后面的?去掉。
  3. 方法e()里不使用u
  4. object: Inter前面加上val x=进行赋值掉。
  5. let最后一行写个1,true或其他明确类型的东西。
【kotlin使用let报java.lang.NoClassDefFoundError】而这样做,报一样错误。
  1. a?.let前面加val c=进行赋值操作。此时就算?(如2所诉) 去掉也报错。结合3,4,5不报错。
把上诉实验通过转换,查看翻译后的Java代码:
// NO.2 public final void f(@NotNull String u) { Intrinsics.checkParameterIsNotNull(u, "u"); Object var2 = this.a; boolean var3 = false; boolean var4 = false; int var6 = false; new T$f$$inlined$let$lambda$1(u); }

//NO.3 public final void f(@NotNull String u) { Intrinsics.checkParameterIsNotNull(u, "u"); if (this.a != null) { boolean var3 = false; boolean var4 = false; int var6 = false; new T$f$1$1(); } else { Object var10000 = null; } }

//NO.4 public final void f(@NotNull String u) { Intrinsics.checkParameterIsNotNull(u, "u"); if (this.a != null) { boolean var3 = false; boolean var4 = false; int var6 = false; new T$f$$inlined$let$lambda$1(u); }}

//NO.5,最后一行加了个true public final void f(@NotNull String u) { Intrinsics.checkParameterIsNotNull(u, "u"); if (this.a != null) { boolean var3 = false; boolean var4 = false; int var6 = false; new T$f$$inlined$let$lambda$1(u); boolean var10000 = true; }}

//NO.6 又出现未知类型:1 public final void f(@NotNull String u) { Intrinsics.checkParameterIsNotNull(u, "u"); Object var3 = this.a; boolean var4 = false; boolean var5 = false; int var7 = false; 1 x = (1)(new T$f$$inlined$let$lambda$1(u)); }

区别 匿名类 是否出现类型(1)
NO.0(原始) T$f$$inlined$let$lambda$1
NO.2 T$f$$inlined$let$lambda$1
NO.3 T$f$1$1
NO.4 T$f$$inlined$let$lambda$1
NO.5 T$f$$inlined$let$lambda$1
NO.6 T$f$$inlined$let$lambda$1
首先分析第4点和第5点。第4点把匿名类对象赋值给了x,这意味这let只能取下一行做返回值(没有下行,就是Unit)。所以也就是说let有明确返回值就不报错。
没有明确返回值类型且“不关心”返回值(如NO.2),也不会错。NO.0也不“关心”返回值啊?可是NO.0又对a的空判断,对let返回又两种结果,要么有返回,要么没返回,将也它归纳为“关心”结果。
至于NO.3的情况(就算结合NO.6也不报错),生成的类就是NO.0中报错中没定义的类(为什么会这样,稍后讨论)。所以也就没问题了。
问题1
为什么let“关心”返回值会有区别?let的返回值究竟是个啥?
分析 首先,查看let方法的定义。
@kotlin.internal.InlineOnly public inline fun T.let(block: (T) -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block(this) }

let 是泛型的扩展方法,参数是个高阶函数。作用是将一种类型T,通过block方法变换变成类型R
所以let返回值是个R。那R是什么呢?
当然是自己定喽。我们一直都是直接用let,实际上严格用法应该这样:
val a = 1 val s = a.let { return@let it.toString() }

只不过,kotlin的自动类型识别帮我们做了类型区分。
问题中的类型1估计就是不明确的R。所以我们手动指定累行试试。
fun f(u: String) { a?.let { object : Inter { override fun a() { print(u) } } } }

对应的Java代码。
public final void f(@NotNull String u) { Intrinsics.checkParameterIsNotNull(u, "u"); if (this.a != null) { boolean var3 = false; boolean var4 = false; int var6 = false; Inter var10000 = (Inter)(new T$f$$inlined$let$lambda$1(u)); } }

原本是1的地方变成了我们指定的类型Inter。代码跑起来也不在跌跟斗。
那为什么“不关心”结果的时候翻译过来就不需要转 R 的类型呢?(目前找到的解释是编译器优化掉明确不需要的过程)
问题2
看这个问题之前,我们应该发现了,let里的代码直接被拷到f()函数里面,而不是生成高阶函数block: (T) -> R表示的接口的实现类。而且Inter本该内部类的实现,也变成了定义了外部独立的类。为什么会这样?
解释
这是因为inline关键字的作用:inline 的工作原理就是将内联函数的函数体复制到调用处实现内联。详情见参考资料。
实验 写一个没有inline的仿let方法,再替换原let
fun T.mylet(block: (T) -> R): R { return block(this) }

//No.7 fun f(u: String) { a?.mylet { object : Inter { override fun a() { print(u) } } } }

运行不报错,看法Java代码。
//NO.7 public final void f(@NotNull final String u) { Intrinsics.checkParameterIsNotNull(u, "u"); Object var10000 = this.a; if (var10000 != null) { var2 = ()TestKt.mylet(var10000, (Function1)(new Function1() { // $FF: synthetic method // $FF: bridge method public Object invoke(Object var1) { return this.invoke(var1); }@NotNull public final invoke(@NotNull Object it) { Intrinsics.checkParameterIsNotNull(it, "it"); return new Inter() { public void a() { String var1 = u; boolean var2 = false; System.out.print(var1); } }; } })); }}

内部用Function1 代表block: (T) -> R表示的接口的实现类。Inter依旧是内部类的方式实现。
可见,inline关键字在处理方法体中的内部类,做了明显的优化处理(即生成一个独立的外部类)。
问题3
结合问题1中的NO.3和问题2以及实验,发现Inter中用到let外部信息时,生成的类是T$f$$inlined$let$lambda$1(但后面还会强转为T$f$1$1,也就是那个1,然后报找不到类的定义的错误),而不使用外部的信息时,生成的类是T$f$1$1。区分度是什么?
猜想
通过inline内联函数传入的lambda表达式生成的匿名类,如果有指向外部的变量,那么命名为:class + method + inlined + method + lambda + number。如果没有,命名为: class + method + number + number(第一个number表示let同层级编号,第二个number表示内部类的编号。而然运行中外部类在执行checkcast的时候,还是按照旧的规则去组装命名。(仅为猜想,未得原因)
结论 其实还没有具体结论!遇到此类问题,大胆猜想,动手实践,总结规律。
kotlin使用let报java.lang.NoClassDefFoundError
文章图片
配一张图

    推荐阅读