Android development is too difficult: Java Lambda ≠ Android Lambda

Posted by drcdeath on Tue, 11 Jan 2022 13:14:32 +0100

This article has authorized the official account of "Hongyang" to be published.

I'm here again and continue to return to writing. My goal is 2 articles in January.

It takes two articles to explain clearly Java Lambda ≠ Android Lambda. This is the previous article. First, explain some knowledge of Java Lambda.

Read this article patiently and you will gain something.

1, Java Lambda is not equal to an anonymous inner class

Test environment JDK8.

First, let's look at a relatively simple code fragment:

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

First ask a simple question. How many class files do you think will be generated if I compile javac?

Needless to say, there must be two. One is testjava lambda Class, one is testjava lambda $1 Class, then try:

Yes, there are two. How can a solid Java foundation be defeated by this problem.

Everyone knows how to write the anonymous inner class above. We can change it to lambda expression, right? Even the compiler will remind you to use lambda. We change it to lambda expression:

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

Another simple question, how many class files do you think will be generated if I compile javac?

Um... Are you fucking me? What's the difference between this and the question just now?

Still think it's two? Shall we try javac again?

Sorry, there is only one class file.

So, here comes my new question:

There must be a difference between the writing of Java anonymous internal classes and the writing of Lambda expressions at compile time. What's the difference?

2, Behind Java Lambda, invokedynamic appears

The first thing to look at this kind of problem is to compare the bytecode. Let's look at the difference between the javap -v method and the test() method:

test() of anonymous inner class:

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

It is very simple that new has a TestJavaAnonymousInnerClass$1 object and then calls its run() method.

An interesting thing is that when calling the construction method, first aload_0, 0 is the current object this. Pass this. This is the secret that anonymous internal classes can hold external class objects. In fact, the current object this is referenced to others.

Let's look at lambda's 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

Unlike the anonymous inner class, it is replaced by an invokedynamic instruction.

If you are familiar with Java bytecode method calls, you should often see a question: what are the differences between invokespecial, invokevirtual, invokeinterface, invokestatic and invokedynamic?

invokespecial also appears in the above bytecode, which generally refers to calling super methods, construction methods, private methods, etc; special means to call some methods to determine the caller.

You may ask, can the caller be uncertain when calling a class method?

Yes, for example, can overloading turn the method call of the parent class into a child class?

Therefore, the calling instruction of non private member methods in the class is generally invokevirtual.

Just understand the literal meaning of invokeinterface and invokestatic.

The general explanation is as follows. If you are confused, just type the section code. For example, are abstract class abstract method calls the same as interface method call instructions? The method modified with final cannot be copied. Will the instruction change?

The last one is invokedynamic:

Generally, it is very rare. We have seen it today. We can see it in Java lambda expressions.

Some in-depth studies can be seen here:

Daily question | is the anonymous inner class in Java written as lambda really just a syntax sugar?

We now know that after using the lambda expression, the bytecode has changed greatly compared with the anonymous inner class, so we are more curious:

What is behind the lambda expression when it runs?

3, lambda expressions are not really generated without inner classes

What is the easiest way to understand the running state of a piece of code?

Um... debug?

Now the IDE is becoming more and more intelligent. Many times, some compilation details of debug are erased.

There is a simple way to stack. Let's modify the following code:

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();
    }
}

Run and look at the error stack:

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)

Let's look at our run method and where it is called:

Well... The final stack is:

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

Is it called by the lambda$test lambda $test $0 method in testjava lambda?

We just made a compilation and missed it. Is there another method? Let's decompile:

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();
}

In this javap -p view, - p means that the private method is also output.

This method is really available. Look at the bytecode of this method:

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

It's very simple. It's the content in our lambda expression {} above. Print a line of log.

Is this method called by test? No, there seems to be a problem with this stack. Let's look back at the stack just now:

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)

Have you found that this stack is too simple, our runnable What about the call stack of run?

This stack should be simplified. Let's add another line of log to see which class we are in when the run() method executes?

We added a line to the run method

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

Look at the output:

com.example.zhanghongyang.blog02.TestJavaLambda

HMM... in fact, we performed a waste operation. At present, the code in this method is put into lambda$test lambda $test $0 ()(). Of course, the output is testjava lambda.

No, I'm going to enlarge.

Let's modify the following method to make the process live longer:

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

After running

Switch to the command line, execute the jps command, and view the pid of the current program process:

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

See 3682 and execute

jstack 3682

It's so touching that I finally found the stack of the hidden run method in this line.

Don't pay too much attention to the JPS and jstack instructions here. They are all provided by jdk. You know it's OK to check the stack. Don't go out and search these two commands. Wait until you finish reading the article.
In addition, the stack can also be obtained through method calls, and the small edge is through reflection Getcallerclass.

Now we have taken another step in the specific truth:

Our lambda$test lambda $test $0 () () method is this object: com example. zhanghongyang. blog02. Called by the run method of testjavalambda $$lambda $1 / 1313922862.

We can draw another conclusion:

The Lambda expression in this paper will help us generate an intermediate class at runtime. The class name format is the original class name $$Lambda $number, and then the call is finally completed through this intermediate class.

Then you may disagree:

You said you would generate at run time? Can you show me?

Well... I'll show you later.

But let's think about another problem first.

4, Missing information in compiled product

We have been saying above:

  1. For the Lambda expression in the example in this paper, no intermediate class is generated during compilation;
  2. The runtime helps us generate intermediate classes;

There is an obvious problem. You didn't generate it for me at compile time, but it was generated at run time; How does it know whether to generate and what kind of classes to generate at runtime? The only class file you compile must contain such information?

That's the truth.

Let's compile javap -v again. At the end of the output information:

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

Sure enough, it contains a piece of information and contains testjava lambda Lambda $test $0 keyword.

You don't have to worry so much. You know that the example of lambda in this article will add a method lambda$test lambda $test $0 () () to the compiled class file, and will carry a message to tell the JVM to create an intermediate class at runtime.

Actually, lambda Metafactory Metafactory is used to generate intermediate classes. There are also related classes in the jdk that can be viewed. We will talk about this in detail later.

5, Take out the middle class?

We have always said that the runtime has generated an intermediate class for us. The class name is probably TestJavaLambda$$Lambda , but it's groundless. We have to take it out before we believe it, right.

Fortunately, it doesn't mean I ate two bowls of jelly

We just said that the JVM helps us generate intermediate classes. In fact, java can take many parameters when running. One of the system attributes is very magical. I'll show you:

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

You see, with the system attribute running, you can dump the generated classes:

Isn't that interesting.

In fact, proxy classes will also be generated in the dynamic proxy, which can also be exported in a similar way.

Then let's take a look at this class. We don't care much about the details of this class. Look directly at the decompiled AS:

That's simple

Therefore, in the examples in this article, Lambda expressions are quite different from anonymous inner classes. You only need to understand:

  1. invokedynamic can be used for lambda;
  2. The intermediate class of Java lambda expression is not absent, but generated at the first run.

As for the performance problem, the impact should be minimal and almost No.

Here's a soul question:

What's the use of these?

After all, I do Android. In fact, I care more about the implementation of lambda in Android, so I'll start with Java Lambda. As for why you ask me to see the implementation of Android Lambda, after all, we often need to insert and grab bytecode piles and customize Transform. We still need to find out the behavior behind some classes.

However, you must note that this article is about the principle of Java lambda.

Don't apply to Android!
Don't apply to Android!
Don't apply to Android!

What's the matter with Android Lambda? I will write a separate article on Android sweetening and D8. I also remember that a colleague was cheated by Android Lambda last time and will write it together.

This paper is based on 1.8.0_ 181.

Goodbye, see you next!

You can add WeChat official account: Hongyang, so that you can receive articles for the first time.

Topics: Java Android Lambda