Android开发太难了:Java Lambda ≠ Android Lambda (上)

本文已授权个人公众号「鸿洋」原创发布。

我又来了,继续回归写作中,目标 1 月 2 篇。

需要两篇才能阐述清楚Java Lambda ≠ Android Lambda,本篇为上篇,先解释清楚 Java Lambda 的一些知识。

耐心阅读本文,你一定会有收获。

一、Java Lambda 不等于 匿名内部类

测试环境JDK8。

首先我们看一段比较简单的代码片段:

public class TestJavaAnonymousInnerClass {
    public void test() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello java lambda");
            }
        };
        runnable.run();
    }
}

先问个简单的问题,如果我javac编译一下,你觉得会生成几个class文件?

不用问,肯定是两个,一个是TestJavaLambda.class,一个是TestJavaLambda$1.class,那么试下:

在这里插入图片描述

没错,确实两个,扎实的Java基础怎么会被这种问题打败。

大家都知道上面这个匿名内部类的写法,我们可以换成lambda表达式的写法对吧,甚至编译器都会提醒你使用lambda,我们改成lambda表达式的写法:

public class TestJavaLambda {
    public void test() {
        Runnable runnable = () -> {
            System.out.println("hello java lambda");
        };
        runnable.run();
    }
}

再问个简单的问题,如果我javac编译一下,你觉得会生成几个class文件?

嗯…你在搞我?这和刚才的问题有啥区别?

还认为是两个吗?我们再javac试一下?

在这里插入图片描述

不好意思,只有一个class文件了。

那么,我的一个新的问题来了:

Java匿名内部类的写法和Lambda表达式的写法,在编译期这么看肯定有区别的,那么有何区别?

二、Java Lambda的背后,invokedynamic的出现

看这类问题,第一件事肯定是对比字节码了,那我们javap -v 一哈,看一下test()方法区别:

匿名内部类的test():

public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class com/example/zhanghongyang/blog02/TestJavaAnonymousInnerClass$1
         3: dup
         4: aload_0
         5: invokespecial #3                  // Method com/example/zhanghongyang/blog02/TestJavaAnonymousInnerClass$1."<init>":(Lcom/example/zhanghongyang/blog02/TestJavaAnonymousInnerClass;)V
         8: astore_1
         9: aload_1
        10: invokeinterface #4,  1            // InterfaceMethod java/lang/Runnable.run:()V
        15: return

很简单,就是new了一个TestJavaAnonymousInnerClass$1对象,然后调用其run()方法。

有个比较有意思的,就是调用构造方法的时候先aload_0,0就是当前对象this,把this传过去了,这个就是匿名内部类可以持有外部类对象的秘密,其实把当前对象this引用给了人家。

再来看lambda的test():

public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=1
         0: invokedynamic #2,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
         5: astore_1
         6: aload_1
         7: invokeinterface #3,  1            // InterfaceMethod java/lang/Runnable.run:()V
        12: return

和匿名内部类不同,取而代之的是一个invokedynamic指令。

如果大家比较熟悉Java字节码方法调用相关,应该经常会看到一个问题:invokespecial,invokevirtual,invokeinterface,invokestatic,invokedynamic有和区别?

invokespecial 其实上面一段字节码上也出现了,一般指的是调用super方法,构造方法,private方法等;special嘛,指定的意思,调用的都是一些确定调用者的方法。

你可能会问,调用一个类的方法,调用者还能有不确定的时候?

有呀,比如重载,是不是能将父类的方法调用转而变成子类的?

所以类中非private成员方法,一般调用指令为invokevirtual。

invokeinterface,invokestatic字面意思理解就可以了。

这块大概解释是这样的,如果有困惑自己打字节码看就好了,例如抽象类抽象方法调用和接口方法调用指令一样吗?加了final修饰的方法不能被复写,指令会有变化吗?

最后一个就是invokedynamic了:

一般很罕见,今天我们也算是见到了,在Java lambda表达式的时候能够见到。

一些深入的研究,可以看这里:

每日一问 | Java中匿名内部类写成 lambda,真的只是语法糖吗?

我们现在知道使用了lambda表达式之后,和匿名内部类去比较,字节码有比较大的变化,那么更好奇了:

lambda表达式运行的时候,背后到底是什么样的呢?

三、lambda表达式不是真的没有内部类生成

想了解一段代码运行时状态,最简单的方式是什么呢?

嗯…debug?

现在IDE都越来越智能了,很多时候debug一些编译细节都给你抹去了。

有个比较简单的方式,打堆栈,我们修改下代码:

public class TestJavaLambda {
    public void test() {
        Runnable runnable = () -> {
            System.out.println("hello java lambda");
            
            int a = 1/0;
        };
        runnable.run();
    }

    public static void main(String[] args) {
        new TestJavaLambda().test();
    }
}

运行下,看下出错的堆栈:

hello java lambda
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.example.zhanghongyang.blog02.TestJavaLambda.lambda$test$0(TestJavaLambda.java:8)
	at com.example.zhanghongyang.blog02.TestJavaLambda.test(TestJavaLambda.java:10)
	at com.example.zhanghongyang.blog02.TestJavaLambda.main(TestJavaLambda.java:14)

看下到底和何方神圣调用的我们的run方法:

嗯…最后的堆栈是:

TestJavaLambda.lambda$test$0(TestJavaLambda.java:8)

是我们TestJavaLambda中的lambda$test$0方法调用的?

是我们刚才发编译看漏了,还有这个方法?我们再反编译看下:

javap /Users/zhanghongyang/repo/KotlinLearn/app/src/main/java/com/example/zhanghongyang/blog02/TestJavaLambda.class 
Compiled from "TestJavaLambda.java"
public class com.example.zhanghongyang.blog02.TestJavaLambda {
  public com.example.zhanghongyang.blog02.TestJavaLambda();
  public void test();
  public static void main(java.lang.String[]);
  private void lambda$test$0();
}

这次javap -p 查看,-p代表private方法也输出出来。

还真有这个方法,看下这个方法的字节码:

private static void lambda$test$0();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #8                  // String hello java lambda
         5: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8

很简单,就是我们上面lambda表达式{}里面的内容,打印一行日志。

那这个方法是test调用的?不对呀,这个堆栈好像有问题,我们在回头看下刚才堆栈:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.example.zhanghongyang.blog02.TestJavaLambda.lambda$test$0(TestJavaLambda.java:8)
	at com.example.zhanghongyang.blog02.TestJavaLambda.test(TestJavaLambda.java:10)
	at com.example.zhanghongyang.blog02.TestJavaLambda.main(TestJavaLambda.java:14)

有没有发现这个堆栈太过于简单了,我们的Runnable.run的调用栈呢?

这个堆栈应该是被简化了,那我们再加一行日志,看下run()方法执行时,自己身处于哪个类?

我们在run方法里面加了一行

System.out.println(this.getClass().getCanonicalName());

看下输出:

com.example.zhanghongyang.blog02.TestJavaLambda

嗯…其实我们执行了一个废操作,当前这个方法里面的代码都被放到lambda$test$0()了,当然输出是TestJavaLambda。

不行了,我要放大招了。

我们修改下方法,让这个进程活的久一点:

public void test() {
    Runnable runnable = () -> {
        System.out.println("hello java lambda");
        System.out.println(this.getClass().getCanonicalName());
        // 新增
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int a = 1 / 0;
    };
    runnable.run();
}

运行后…

切到命令行,执行jps命令,查看当前程序进程的pid:

java zhanghongyang$ jps
99315 GradleDaemon
3682 TestJavaLambda
21298 Main
3685 Jps
3258 GradleDaemon
1275 
3261 KotlinCompileDaemon

看到了3682,然后执行

jstack 3682

在这里插入图片描述

太感人了,终于把这行隐藏的run方法的堆栈找出来了。

这里大家不要太在意jps,jstack这些指令,都是jdk自带的,你就知道能查堆栈就行了,别出去搜这两个命令去啦,文章看完再说。
另外获取堆栈其实也能通过方法调用,小缘是通过Reflection.getCallerClass看的。

到现在我们具体真相又进了一步:

我们lambda$test$0()方法是这个对象:com.example.zhanghongyang.blog02.TestJavaLambda$$Lambda$1/1313922862的run方法调用的。

我们又能下个结论了:

文中lambda表达式的写法,在运行时,会帮我们生成中间类,类名格式为 原类名$$Lambda$数字,然后通过这个中间类最终完成调用。

那么你可能表示不服:

你说运行时生成就生成呀?你拿出来给我看看?

嗯…等会我拿出来给你看。

不过我们先思考另一个问题。

四、编译产物中遗漏的信息

上文我们一直在说:

  1. 对于文中例子中的Lambda表达式编译时没有生成中间类;
  2. 运行时帮我们生成了中间类;

那有个很明显的问题,编译时你没给我生成,运行时生成了;运行时它怎么知道要不要生成,生成什么样的类,你编译产物就那一个class文件,里面肯定要包含这类信息的呀?

是这么个道理。

我们再次发编译javap -v查看,在输出信息的最后:

SourceFile: "TestJavaLambda.java"
InnerClasses:
     public static final #78= #77 of #81; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #35 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #36 ()V
      #37 invokespecial com/example/zhanghongyang/blog02/TestJavaLambda.lambda$test$0:()V
      #36 ()V

果然包含一段信息,而且包含TestJavaLambda.lambda$test$0关键词。

大家不用管那么多,你就知道,文中lambda的例子,会在编译的class文件中新增一个方法lambda$test$0(),并且会携带一段信息告知JVM在运行时创建一个中间class。

其实LambdaMetafactory.metafactory正是用来生成中间class的,jdk中也有相关类可以查看,后续我们再详细说这个。

五、把中间类拿出来看看?

我们一直说运行时帮我们生成了一个中间类,类名大概为:TestJavaLambda$$Lambda$1,但是口说无凭,得拿出来大伙才信,对吧。

还好不是说我吃了两碗凉粉…

我们刚才说了JVM帮我们生成了中间类,其实java在运行的时候可以带很多参数,其中有个系统属性很神奇,我用给你们看:

java -Djdk.internal.lambda.dumpProxyClasses com.example.zhanghongyang.blog02.TestJavaLambda

懂了吧,加上这个系统属性运行,可以dump出生成的类:
在这里插入图片描述

是不是有点意思。

其实动态代理中间也会生成代理类,也可以通过类似方式导出。

然后我们看看这个类呗,这个类我们就不太在乎细节了,直接AS里面看反编译之后的:

在这里插入图片描述

真简单…

所以,本文中的例子,Lambda表达式和匿名内部类的区别还是挺大的,大家只要了解:

  1. invokedynamic可以用于lambda;
  2. Java lambda表达式的中间类并不是没有,而是在首次运行时生成的。

至于性能问题,影响应该是微乎其微的,几乎没有的。

下面有个灵魂一问:

你看这些有啥用?

毕竟我是搞Android的,其实我更在乎Android中lambda的实现,所以就先以Java Lambda为开始了,至于你问我为啥要看Android Lambda实现,毕竟现在经常要字节码插抓桩,自定义Transform,对于一些类背后的行为还是要搞清楚的。

但是,大家一定要注意,本文讲的是 Java lambda 的原理。

不要套用到Android上!
不要套用到Android上!
不要套用到Android上!

那 Android Lambda 是怎么一回事,后续会单独写一篇,Android 脱糖与D8 的一些事儿,还想起来上次有个同事被Android Lambda 坑了一次,会一起写出来。

本文基于1.8.0_181。

告辞,下篇见!

你可以添加微信公众号:鸿洋,这样可以第一时间接收文章。

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
### 回答1: 这是一个创建新线程的语法,使用lambda表达式定义新线程的操作。具体来说,std::thread是C++11中的线程库,可以用来创建和管理线程。lambda表达式则是一种匿名函数,可以在创建线程时指定要执行的操作。这样,我们就可以将一个函数或一段代码作为参数传递给std::thread的构造函数,创建一个新的线程来执行这些操作。例如,以下代码创建了一个新线程,并在该线程中执行一个lambda表达式: ``` std::thread t([](){ // 执行一些操作 }); ``` 在实际使用中,我们可以将需要并发执行的任务封装在lambda表达式中,然后使用std::thread创建多个线程来同时执行这些任务,从而提高程序的执行效率。 ### 回答2: std::thread lambda是C++11引入的标准库中的一个功能,它允许我们使用lambda表达式创建一个线程。 在C++中,线程是独立执行的代码片段,它可以同时执行多个任务。使用std::thread类,我们可以创建一个新的线程并指定其执行的代码。而lambda表达式是C++11引入的一个便捷的语法,可以在函数内部定义一个匿名函数,并可以直接使用该函数,而不必声明一个具名函数。 要使用std::thread lambda,需要包含 <thread> 头文件。然后我们可以使用lambda表达式作为std::thread的第一个参数,来创建一个新的线程。 示例代码如下所示: ```cpp #include <iostream> #include <thread> int main() { std::thread t([](){ std::cout << "Hello from thread!" << std::endl; }); t.join(); // 等待线程完成 return 0; } ``` 在上面的例子中,我们使用lambda表达式创建了一个匿名函数,并将其作为std::thread的第一个参数传递。该线程会调用lambda表达式内部的代码,即输出 "Hello from thread!"。然后我们调用t.join()函数,等待线程完成。 使用std::thread lambda可以方便地创建并执行简单的线程任务,减少了定义具名函数的繁琐过程。同时,lambda表达式还可以捕获外部变量,使得线程内部可以访问外部作用域的变量。这使得我们可以更方便地在线程中使用外部变量。 总而言之,std::thread lambda提供了一种简洁、方便的创建线程的方式,使得多线程编程在C++中更加容易和灵活。 ### 回答3: std::thread lambda是C++标准库中的一个功能,用于创建并启动一个线程,并使用lambda表达式作为线程的执行函数。 lambda表达式是一种函数对象,可以在代码中以匿名函数的形式使用。它主要由参数列表、函数体和捕获列表组成。通过使用lambda表达式,我们可以将一段代码作为线程的执行函数,更加灵活方便地实现多线程编程。 使用std::thread lambda,我们首先需要创建一个std::thread对象,并将lambda表达式作为参数传递给std::thread的构造函数。例如: ```cpp std::thread myThread([](){ // lambda表达式的函数体 // 在这里编写线程的具体执行代码 }); ``` 在创建std::thread对象时,lambda表达式会被复制,并被存储在新线程的执行环境中。之后,我们可以调用std::thread对象的成员函数,如join()或detach(),来等待或分离线程的执行。 需要注意的是,lambda表达式可以在函数体内捕获一些变量,并在线程执行时访问这些变量。这些被捕获的变量可以是值传递、引用传递或引用传递,具体取决于捕获列表中的符号(`=`, `&`等)。这样,我们可以在lambda表达式中使用外部变量,并与线程产生交互。 总而言之,std::thread lambda是一种在C++中创建和启动线程的方式,它使用lambda表达式作为线程的执行函数,使得多线程编程更加方便和灵活。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值