In-depth understanding of Java's Lambda principle

Original link:

Article Directory

  • Background on Lambda Expressions
  • pass a piece of code
    • Anonymous inner class writing method
    • Lambda writing
  • The actual operation of the two writing methods
    • Anonymous inner classes in action
    • Practical operation of Lambda writing
  • Lambda Expressions vs Anonymous Inner Classes
  • Summarize

Background on Lambda Expressions

The concept of Lambda function has a long history, and it has been implemented in Lisa and C#. And in recent years, developers have higher requirements for the expressiveness of the language. Java also introduced the concept of Lambda function in JDK 1.8. Although it has been seven years since I wrote this text, the underlying design ideas are still worthy of our reference so that we can use it better.
The core idea of ​​Lambda expression is to pass a function as a parameter. So how to achieve this goal? Let's take it slowly.

pass a piece of code

Passing a function as a parameter is easy to implement in C. For function pointers, one or two * signs & signs can solve the problem. The language characteristics of C have inherent advantages.
But what about Java? We all know that the parameters received by Java functions are all objects (at this time, the basic type can also be considered as an object), but we have never heard of the term function object. How to fix it?
Imagine the following scenario, the function executeFunc, which receives a function, uses the received function to print another incoming variable word.
The knowledge points of the article are matched with the official knowledge files, and relevant knowledge can be further learned

static void executeFunc(方法??,String word) {
    // todo 用传入的方法打印word变量
}

Anonymous inner class writing method

Let’s introduce a way to save the country with a curve. The industrious and brave programmers came up with it. It’s not enough to pass the function directly, so I’ll wrap this method with an interface, and just pass the interface object in! Good idea, let's take a look at the code below.

package main;

interface Wrapper {
    
    
    void myPrint(String w);
}

class Solution {
    
    

    static void executeFunc(Wrapper w, String word) {
    
    
        w.myPrint(word);
    }


}

Indeed, problem solved. Then how to call it in the main function?

public static void main(String[] args) {
    
    
        executeFunc(new Wrapper() {
    
    
            @Override
            public void myPrint(String w) {
    
    
                // 个性化拓展,例如在打印之前记录时间什么的
                System.out.println(w);
            }
        }, "Hello Lambda!");
    }

It seems a little troublesome. If you need to use executeFunc more times, it will obviously cause a lot of "unnecessary" code. It is not necessary to say here because we must meet the requirements of the compiler to write a standardized grammar, but in fact the logic we care about is only the short printed statement inside:

// 个性化拓展,例如在打印之前记录时间什么的
System.out.println(w);

This is where lambdas come in handy.

Lambda writing

In fact, if you write the above code in the compiler, you will receive a gray smart prompt, allowing you to rewrite the above statement with lambda.

Let's apply this modification and try it out.

public static void main(String[] args) {
    
    
    executeFunc(w -> {
    
    
        // 个性化拓展,例如在打印之前记录时间什么的
        System.out.println(w);
    }, "Hello Lambda!");
}

We see that a large series of new operations, @Override rewriting is replaced by an incoming parameter, a pair of curly braces, and the amount of code is greatly reduced. (Actually, it can be further simplified by method reference, but we will not discuss it here)

The actual operation of the two writing methods

Finally, I have introduced two common ways of writing. After reading the above writing, some people may say, doesn’t this look similar? Lambda just helped us complete the rewriting of things like override, syntactic sugar, sweet, that’s it?
No, Lambda is an important upgrade feature of Java 8. In order to automatically generate this statement, the compiler needs to install a plug-in or write a macro definition. Why take so much effort to toss? Let's analyze the difference between the two in detail. The following will involve some bytecode understanding, but don't worry, I will explain the bytecode instructions to be used (I have also suffered from it).
The complete code is as follows, and we analyze it based on the following code.

package main;

interface Wrapper {
    
    
    void myPrint(String w);
}

class Solution {
    
    

    static void executeFunc(Wrapper w, String word) {
    
    
        w.myPrint(word);
    }

    public static void main(String[] args) {
    
    
        // 匿名内部类写法
        executeFunc(new Wrapper() {
    
    
            @Override
            public void myPrint(String w) {
    
    
                // 个性化拓展,例如在打印之前记录时间什么的
                System.out.println(w);
            }
        }, "Hello Lambda!");
        
        // lambda写法
        executeFunc(w -> {
    
    
            // 个性化拓展,例如在打印之前记录时间什么的
            System.out.println(w);
        }, "Hello Lambda!");
 
    }
}

Anonymous inner classes in action

Let's comment out the lambda part first, and only keep the statement written in the anonymous inner class. Use IDEA's ShowByteCode function to view it. Bytecode (ByteCode) is read and executed by the JVM at runtime, and all grammatical features will be fully exposed here. First paste a piece of bytecode below.

// class version 55.0 (55)
// access flags 0x20
class main/Solution {
    
    

  // compiled from: Solution.java
  NESTMEMBER main/Solution$1
  // access flags 0x0
  INNERCLASS main/Solution$1 null null

  // access flags 0x0
  <init>()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lmain/Solution; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x8
  static executeFunc(Lmain/Wrapper;Ljava/lang/String;)V
   L0
    LINENUMBER 10 L0
    ALOAD 0
    ALOAD 1
    INVOKEINTERFACE main/Wrapper.myPrint (Ljava/lang/String;)V (itf)
   L1
    LINENUMBER 11 L1
    RETURN
   L2
    LOCALVARIABLE w Lmain/Wrapper; L0 L2 0
    LOCALVARIABLE word Ljava/lang/String; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 15 L0
    NEW main/Solution$1
    DUP
    INVOKESPECIAL main/Solution$1.<init> ()V
    LDC "Hello Lambda!"
    INVOKESTATIC main/Solution.executeFunc (Lmain/Wrapper;Ljava/lang/String;)V
   L1
    LINENUMBER 29 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}


The JVM doesn't care about your internal classes, it only recognizes classes and objects instantiated by classes. As we all know, interfaces cannot be instantiated, that is to say, it is not possible to create a pure interface object by direct new, but to write a class to implement the interface, and then instantiate this class. Then notice our statement above

//*****
new Wrapper() {
    
    
            @Override
            public void myPrint(String w) {
    
    
                // 个性化拓展,例如在打印之前记录时间什么的
                System.out.println(w);
            }
        }
//*****

Doesn't this violate the rule? actually not. It seems to be written like this, but in fact the object is still instantiated through the class, but this work is done by the JVM for us.
Going back to the bytecode, on the sixth line, we found that the compiler created a new class called Solution$1. The new operation is performed on line 42, and a new Solution 1 object is created for subsequent operations. The initialization of this object is carried out on line 44, init, everyone knows it. Originally, the name of the inner class should be an object similar to S olution 1 for subsequent operations. The initialization of this object is carried out on line 44, init, everyone understands it. Originally the name of the inner class should be something like Solution1 for subsequent operations. The initialization of this object is carried out on line 44, init , everyone understands it. Originally, the name of the internal class should be named like S o lu t i o n MyInnerClass , the parent class Solution before the $ symbol, and MyInnerClass is the name of the internal class. But since what we created isan anonymous inner class that implements the Wrapper interface, anonymous has no name, the compiler thinks, just call it 1, yes, just use 1 to name it. What? Didn't you say you can't name classes with numbers? Note that we usually write programming for the boss (JVM), so there is a large list of grammatical rules. The boss read it and said OK.
If you don’t believe me, just add a statement to the implementation of the anonymous inner class, get the class name of the current anonymous inner class through reflection, and you will find that this inner class is indeed called 1, and the full name ismain.Solution$1

// 匿名内部类写法
executeFunc(new Wrapper() {
    
    
    @Override
    public void myPrint(String w) {
    
    
        // this指向当前匿名内部类的对象,通过对象获取类名
        System.out.println(this.getClass().getName());
        // 个性化拓展,例如在打印之前记录时间什么的
        System.out.println(w);
    }
}, "Hello Lambda!");

output result

C:\jdk11\bin\java.exe....
main.Solution$1
Hello Lambda!

Process finished with exit code 0

Through the above example, we can also understand what the word "anonymous" in an anonymous inner class means.

Practical operation of Lambda writing

So what happens when you use Lambda expressions? Still in the previous code, we comment out the implementation of the anonymous inner class and view its bytecode.

// class version 55.0 (55)
// access flags 0x20
class main/Solution {
    
    

  // compiled from: Solution.java
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x0
  <init>()V
   L0
    LINENUMBER 8 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lmain/Solution; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x8
  static executeFunc(Lmain/Wrapper;Ljava/lang/String;)V
   L0
    LINENUMBER 11 L0
    ALOAD 0
    ALOAD 1
    INVOKEINTERFACE main/Wrapper.myPrint (Ljava/lang/String;)V (itf)
   L1
    LINENUMBER 12 L1
    RETURN
   L2
    LOCALVARIABLE w Lmain/Wrapper; L0 L2 0
    LOCALVARIABLE word Ljava/lang/String; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 26 L0
    INVOKEDYNAMIC myPrint()Lmain/Wrapper; [
      // handle kind 0x6 : 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;
      // arguments:
      (Ljava/lang/String;)V, 
      // handle kind 0x6 : INVOKESTATIC
      main/Solution.lambda$main$0(Ljava/lang/String;)V, 
      (Ljava/lang/String;)V
    ]
    LDC "Hello Lambda!"
    INVOKESTATIC main/Solution.executeFunc (Lmain/Wrapper;Ljava/lang/String;)V
   L1
    LINENUMBER 31 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x100A
  private static synthetic lambda$main$0(Ljava/lang/String;)V
   L0
    LINENUMBER 28 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 0
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 29 L1
    RETURN
   L2
    LOCALVARIABLE w Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}


The bytecode is a bit long, but there are only a few key points, let's take a closer look.
First see this place on line 61.

private static synthetic lambda$main$0(Ljava/lang/String;)V

This thing that looks like a function is actually a function. The word synthetic originally means "artificial; (artificial) synthetic; comprehensive (type);" as a keyword, it means that the method is compiled. generated automatically. In short, if you treat it as void, it's easy to read. This is a static method with no return value. The name has been determined. It is called lambda$main$0 and accepts a parameter of type String. There are a series of operations inside, such as println.
Eh? Think about it carefully, isn't it very similar to what we wrote before? That's right, that lambda expression.

w -> {
    
    
    // 个性化拓展,例如在打印之前记录时间什么的
    System.out.println(w);
}
// 就是上面这货

The compiler converts the lambda expression we wrote into a static private function , and solves the problem of passing a piece of code by calling this function. Everything is clear~
We can try to use the this keyword inside the lambda expression, trying to get the class name of the current class like the anonymous inner class before. In fact, you can also find some clues through the IDE at this time, it will prompt you

You cannot put this under a static context. According to the above analysis, this "context" refers to a static function.
Wait, there is another question, how does the JVM know that when it needs to run this lambda function, it will execute it? This is what the JDK does at the bottom, adding the instruction INVOKEDYNAMIC.
We see line 41, there is an INVOKEDYNAMIC instruction, which is where the lambda expression we wrote performs the initial conversion. We definitely don't want to call immediately where we write lambda expressions, because there is no meaning of "passing code". Therefore, jdk inserts this instruction for us to tell the JVM not to be troublesome when it runs here Run this function, only when necessary.
The INVOKEDYNAMIC command, as the name suggests, is dynamic activation. So how is this dynamic understood? "Dynamic" here means that it is determined at runtime where the next step of calling the lambda function points. In the initial state (when the program is compiled into bytecode but not running), this flag is empty and no action will be performed. When the program starts executing, specifically line 43 of the bytecode, initialization begins. It is determined at runtime that it is nothing more than reflection. If you look carefully at a large number of packets in line 43, it is indeed realized through reflection. I won't go into more details, lest everyone get tired of reading it, and go back to our code layer. To make a digression, I have always felt that it is difficult and boring to learn something completely out of the existing knowledge system, but it is often happy to expand on the basis of the existing knowledge system.
(If you want to know more about invokedynamic, you can refer to the The invokedynamic Instruction chapter of Oracle's official document.
) So far, the analysis of the above two "passing a piece of code" has been completed, and we finally know how the above two methods realize the function of passing code that's it. So why did JDK choose the Lambda implementation? Why not bother introducing new directives? Let's analyze the difference between the two implementation methods.

Lambda Expressions vs Anonymous Inner Classes

Let's talk about anonymous inner classes first. Through the above analysis, we know that the implementation of anonymous inner class for passing a piece of code is actually by creating an inner class that implements the interface. We hand over this work to the compiler, so it is anonymous. We just create a new object , passed to the function that needs this code. Then its advantages and disadvantages are closely related to the inner class.
Objects generated by inner classes will hold references to outer objects by default. In fact, it is not difficult to understand. It is said that it is internal. If you don’t say yes, how do you know whose internal is? This method of implementing code transfer has brought certain benefits, according to Oracle's official statement

  • It is a way of logically grouping classes that are only used in one place: If a class is useful to only one other class, then it is logical to embed it in that class and keep the two together. Nesting such “helper classes” makes their package more streamlined.
  • It increases encapsulation: Consider two top-level classes, A and B, where B needs access to members of A that would otherwise be declared private. By hiding class B within class A, A’s members can be declared private and B can access them. In addition, B itself can be hidden from the outside world.
  • It can lead to more readable and maintainable code: Nesting small classes within top-level classes places the code closer to where it is used.

The general idea is that the design is more scientific, the logic is more reasonable, and the readability is stronger.
In practice, everyone seems to not pay much attention to these features, which is normal, because "passing a piece of code" is such a simple job, you tell me this?
In addition, due to the permission mechanism of Java, the inner class can easily access the variables of the outer class, as shown in the following code, using outerPhone in the myPrint method of InnerClass will not cause any errors.

// 内部类很方便
class Solution {
    
    
    String outerPhone = "13721211111";
    
    private class InnerClass implements Wrapper {
    
    
        
        @Override
        public void myPrint(String w) {
    
    
            System.out.println("正在打给"+outerPhone);
        }
    }
}

But the disadvantages it brings are a bit unacceptable for us. As mentioned earlier, after the anonymous inner class is compiled by the compiler, an actual class will be generated in the bytecode. Then this class must follow a series of regulations of the JVM, and the execution includes loading, verification, preparation, and initialization. process. This process is often relatively expensive. Imagine that, like the situation where lambdas are used extensively in Android development, if they are implemented with internal classes, a large number of classes will be generated at compile time, and this overhead cannot be ignored.
Second, as mentioned earlier, anonymous inner classes hold references to outer objects by default. There may be a memory leak problem caused by the life cycle here, and the attention is "possible". Students who are familiar with Android development should know about the memory leak problem of the handler, which highlights the disadvantages of using anonymous inner classes to implement code transfer logic. Those who have not learned about Android development can imagine such a situation: a time-consuming operation is performed in the method of the anonymous inner class. Because of the time-consuming, the anonymous object cannot be recycled for a long time, and its outer class object has been discarded. Yes, the system should have recycled this thing, but because it is held by that time-consuming anonymous object, it cannot be recycled, which will lead to a memory leak. This is very deadly.
The Lambda writing method does not have this problem, because it is static and does not hold external objects. In fact, in addition to the conversion to static method execution mentioned above, the Lambda writing method also has an implementation of conversion to static inner classes. You can see the bold static word, I think this is the biggest difference between Lambda and anonymous inner class.

Summarize

After talking about a lot of differences between the two, the conclusion is that if you can use lambda, use lambda as much as possible. Not only good-looking, but also efficient and easy to use. In my opinion, the dynamic performance brought by lambda is revolutionary. It simplifies a lot of development work and can have a higher level of abstraction. There are more and more application examples in Android development. If you really fall in love with Lambda, then I suggest you use Kotlin as your preferred programming language, because Kotlin's Lambda really treats a function as an object , and the underlying support is more complete.


Original link: https://blog.csdn.net/weixin_43687181/article/details/116053262

Guess you like

Origin blog.csdn.net/riding_horse/article/details/130060186