preface

In our daily work, we are familiar with lambda expressions, but we have not had a clear and systematic understanding of lambda, so this article will sort it out carefully.

The body of the

When we use Kotlin, such as using lambdas to pass parameters to functions or passing filters to sets and processes, you might think that this code is perfectly normal, but it has not been thoroughly analyzed. In this article, we will introduce it systematically.

Why did Java8 introduce lambda

Before we used Java 8, lambda was not a concept, so if we wanted to write a button to listen for a callback, we could only do this:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v){}});Copy the code

This is too cumbersome to write, but when using Java8, it can be simplified to the following code:

button.setOnClickListener(v -> {

});
Copy the code

You might take this for granted, but the introduction of lambda in Java8 is what many expected.

In fact, Java8 introduced lambda to solve this single method interface, so that the tedious code to write anonymous inner class optimization, concise some.

The definition of lambda

Lambda expressions are called lambda for short. First of all, it is an expression, and the most obvious difference between an expression and a statement is that an expression has values, whereas a statement does not.

Second, it is a piece of code, and it has a fixed format, which is important, as follows:

Note the syntax here, which must be enclosed in curly parentheses. Parameters are not enclosed in curly parentheses, and are distinguished from function types.

Kotlin calls the Java function

Earlier we looked at the improvements made to the single-method interface in Java8, but a lot of our code is written by Kotlin, and the library code is in Java, so it is critical that Kotlin can call the above code, as in the following example:

// Single-method callback uses Java to define a single-method interface
interface SingleFunListener {
    public int singleFunction(int k);
}
Copy the code
// An instance of the interface
private SingleFunListener singleFunctionListener;
Copy the code
// Set the listening method
public void setSingleFunctionListener(SingleFunListener singleFunctionListener){
    this.singleFunctionListener = singleFunctionListener;
}
Copy the code

Now let’s use Kotlin to call the above code, which we are most familiar with in Java:

// Use anonymous inner classes
val testJava = TestJava()
testJava.setSingleFunctionListener(object: SingleFunListener{
    override fun singleFunction(k: Int): Int {
        return 100}})Copy the code

Since Kotlin can use higher-order functions and treat functions as values, we can define a function as a parameter:

// Use functional programming
val listener:(Int) -> Int = {
    100
}
testJava.setSingleFunctionListener(listener)
Copy the code

The above code can be further simplified:

// Use lambda expressions
testJava.setSingleFunctionListener { k: Int ->
    100
}
Copy the code

Note that the use of higher-order functions is similar to lambda, but with a difference that will be discussed later.

Java functional interfaces

Lambdas are useful examples, but why should java-defined interfaces, such as Kotlin or Java 8, use lambda instead of writing anonymous inner classes

When an interface is defined with only one abstract method, it is called a function-time interface, or SAM interface, which stands for abstract method. For example, Runnable and Callable interfaces in Java.

Kotlin allows lambdas to be used when calling functional interfaces as arguments to keep code clean.

Note that Kotlin has full function types, so Kotlin functions that need to shuttle lambda as an argument should use function types instead of functional interface types. So Kotlin does not support automatic conversion of lambdas to objects that implement kotlin interfaces, so SAM interfaces written in Kotlin cannot use lambdas.

Functional interfaces VS function types

The interesting thing here is that for the SAM interface, Kotlin can simplify the code by converting the lambda to the corresponding INSTANCE of the SAM interface. This is actually a convention, but when Kotlin code defines the SAM interface, it cannot be converted because Kotlin has its own function type.

Let’s go straight to an example:

//KT file single method interface
interface OneMethodListener{
    fun onMethod(a:Int)
}

// Interface instance
private var listener:OneMethodListener? = null

// Set the listener
fun setOnMethodListener(listener:OneMethodListener){
    this.listener = listener
}
Copy the code

I call setOnMethodListener here to remind us to convert to a lambda, so let’s see:

The IDE doesn’t remind us that we can change it to lambda, which simply doesn’t support conventions, because in Kotlin, this SAM interface can be implemented using higher-order functions, such as the following code:

// Higher order function
private var onMethodListener:((Int) -> Unit)? = null

fun setMethodListener(listener:((Int) -> Unit)?){
    this.onMethodListener = listener
}
Copy the code

For code that does the same thing, we call:

This is where lambdas can be passed to variables of function type, so the key thing to remember is that with Kotlin, there is no need to define SAM interfaces, simplifying the code.

Kotlin wants you to do this, and doesn’t want you to turn lambda directly into an instance of the SAM interface Kotlin defines.

Higher-order functions

Now that we have seen above that a function type variable defined can pass a lambda, it is important to say what a higher-order function is.

A higher-order function is simply a function whose argument is another function or whose return value is another function, so how do you pass a function here is the variable that defines the type of the function.

For example:

private var onMethodListener:((Int) -> Unit)? = null
Copy the code

The type here is the function type, but the most important thing here is that in Kotlin, higher-order functions can be represented as lambda, which is also the convention, so you expect to pass parameters of the function type, which can be lambda instead.

Access variables in scope

In Java, we use anonymous inner classes in methods or lambdas are local variables that cannot be used directly, as in the following code:

The reason why lambda cannot be accessed and modified is very simple. The principle of lambda is actually anonymous inner class. According to the Java life cycle, we know that the collection of reference type objects is controlled by GC, but the method life cycle will be introduced when the method is finished executing. So calling a local variable of a method in a lambda expression or anonymous inner class is not allowed.

But in Kotlin, Kotlin allows non-final variables of a method to be accessed inside the lambda and even modified, as in the following code:

// Here access to tempString in lambda is normally accessible and modified
val testJava = TestJava()
var tempString = "zyh"
testJava.setSingleFunctionListener {
    tempString = "wy"
    100
}
Copy the code

Isn’t it strange to see here, how Kotlin did it?

capture

Whether Java or Kotlin, external variables accessed from a lambda are said to be captured by the lambda, such as the tempString variable above.

The reason is the lifecycle of a variable. If a variable is captured by a lambda, the code that uses that variable can be stored and executed later. In fact, the variable is stored in a different place, and the function life cycle is normal.

When capturing a final variable, its value is stored with the lambda code that uses the value, while for a non-final variable, its value can be changed in the lambda by encapsulating its value ina special package container that can change the value. References to the package container are stored with the lambda code.

In fact, the principle of capture is very easy. For unmodifiable variables, just pull them out and store them with the lambda, and for modifiable variables, encapsulate them, so that the collection of references is handled by GC, just like the anonymous inner class, and that solves the problem.

Kotlin capture

Now that you know how to capture, Kotlin’s capture is just a refinement of Java implementation details, such as the val variable’s value being copied when captured, and the var variable’s value being saved as an instance of the Ref class when captured.

Functions that pass lambda arguments to Java SAM type arguments

We said that Kotlin has a function type, so for Java’s SAM interface as a function, Kotlin can be passed to the lambda, but how it works is very simple kotlin converts the lambda to an instance of the corresponding SAM interface.

However, this is a bit different from the normal anonymous inner class writing. Instead of creating an instance each time, it is similar to creating a variable of an anonymous inner class in Java and then using that variable over and over again.

// Here Kotlin calls the Java SAM argument function using lambda
val testJava = TestJava()
testJava.setSingleFunctionListener {
    100
}
Copy the code

The above code is equivalent to the following code:

val singleListener : SingleFunListener = object: SingleFunListener{
    override fun singleFunction(k: Int): Int {
        return 100
    }

}
testJava.setSingleFunctionListener(singleListener)
Copy the code

As you can see, lambda can create several fewer interface instance variables when no variables are captured.

But when a variable is captured, this changes and becomes the new instance for the simple reason that it needs to put the captured variable into the auto-generated class.

The principle here, since it is done by the compiler, is also described in the Kotlin capture principle, which is similar to the following:

// The tempString variable is captured here
val testJava = TestJava()
var tempString = "zyh"
testJava.setSingleFunctionListener {
    tempString = "wy"
    100
}
Copy the code

Since the variables captured by lambda are different in different functions, the compiler automatically generates different instances, similar to the following:

// The tempString is captured here
val singleListener : SingleFunListener$1 = object: SingleFunListener$1(var tempString: String){
    override fun singleFunction(k: Int): Int {
        tempString = "wy"
        return 100
    }

}
testJava.setSingleFunctionListener(singleListener)
Copy the code

If other variables are captured in another function, the generated interface will be different, so there will be multiple instances.

Note that lambda’s conversion to SAM only applies when Kotlin calls Java’s functional methods, but more on that later.

SAM construction method

Kotlin can pass a lambda to the SAM interface for convenience, but when the return value is the SAM interface, it needs to create an instance of the SAM interface through the lambda. In this case, the SAM constructor can be used.

Here’s a quick example:

// A single method callback, which returns an instance of type Runnable
interface SingleFunListener {
    public Runnable singleFunction(int k);
}

// Set method
public void setSingleFunctionListener(SingleFunListener singleFunctionListener){
    this.singleFunctionListener = singleFunctionListener;
}
Copy the code

This code can be easily solved using an anonymous inner class. Create a class that implements the Runnable interface:

testJava.setSingleFunctionListener(object: SingleFunListener{
    override fun singleFunction(k: Int): Runnable {
        return Runnable { Logger.d("zyh")}}})Copy the code

But how could an elegant lambda still make you write so much code? It can be simplified as follows:

// Pass lambda directly to the SAM interface
testJava.setSingleFunctionListener {
    Runnable { Logger.d("zyh")}}Copy the code

Runnable is an interface, and it definitely has no constructor.

Java uses function types

When Kotlin calls Java’s SAM interface, the lambda is converted to an instance of the SAM interface. How does Kotlin call a function type parameter in Java

//Kotlin code defines a higher-order function
private var onMethodListener:((Int) -> Unit)? = null

fun setMethodListener(listener:((Int) -> Unit)?){
    this.onMethodListener = listener
}
Copy the code

We then call this function in our Java code:

public void runTestJava(){
    TestLambda testLambda = new TestLambda();
    // Call the method of the function type argument
    testLambda.setMethodListener(new Function1<Integer, Unit>() {
        @Override
        public Unit invoke(Integer integer) {
            return null; }}); }Copy the code

Kotlin’s high-order function is actually the implementation of the FunctionN interface. Each interface has an invoke method, and the invoke method can be invoked to call the function itself.


/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}

//....

/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
    /** Invokes the function with the specified arguments. */
    public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}
Copy the code

You’ll find that you can only pass in 22 arguments at most, haha, but you don’t normally pass that many. This is how higher-order functions work, and it’s actually quite simple, an implementation of the FunctionN interface that you define.

The functional API for collections

Previously we talked about the implementation principle of Kotlin’s higher-order functions, so now let’s talk about one of the most commonly used functions in our code, which is the set sum functional API.

There are many methods. Here we take filter as an example:

// Use the code
val ints = arrayListOf(1.2.3.4.5)
val newInts = ints.filter { it > 2 }
Copy the code
/ / the source code
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}
Copy the code

If we look at the source code, first of all, it’s an extension function, and then the predicate argument is a function type, so we can pass lambda, and the return value has to be a Boolean, which we’ve seen before, notice that there’s multiple inline, so let’s look at this inline.

Inline function

A function that is inline is called an inline function. Why should filter be inline

The function type is an instance of the FunctionN interface, so the lambda passed by filter is compiled into an anonymous inner class.

// Sets and elements are iterated over, with each element called once as a function type parameter
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}
Copy the code

There is a problem here, when the set sum has 100 elements, it creates 100 anonymous inner classes, which is a serious performance problem, so the concept of inline functions is introduced here.

With that said, the inline function copies the implementation of the function to the call location, so that additional anonymous inner class instances are not created.

Lambda with receiver

In the compiler’s view, a lambda is a block of code, so this will point to the class that surrounds it. Here’s a unique feature of Kotlin’s lambda, lambda with receiver, In this lambda our this and it refer to other class instances.

For example:

val ints = arrayListOf(1.2.3.4.5)
ints.apply { 
    this.add(6)}Copy the code

Here are the apply and with functions, both of which we are familiar with, but why use this inside the lambda? In case you haven’t noticed, let’s take a look at the source code:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
Copy the code

You’ll notice that the function defined here is fairly generic, but one difference is that the block type, which is T.() -> Unit, not normal () -> Unit, is unique to Kotlin. This is where the function type parameter is defined and it has an acceptor, You can use this in a lambda to access the recipient.

For example, in the Apply source code above, the type parameter T is the receiver, and the receiver’s properties and methods are accessible in lambda.

conclusion

In fact, there is a lot of lambda knowledge, many details need to be paid attention to, they are the key to Kotlin’s introduction code.

Let’s briefly review some of the details:

  • Lambda was introduced in Java 8 to get around the tedious process of writing anonymous inner classes as arguments to Java’s single-method interface.
  • Single-method interfaces are called SAM interfaces or functional interfaces.
  • Kotlin calls Java’s SAM interface argument pass lambda, which is converted to an instance of the corresponding SAM interface.
  • Kotlin can use the SAM interface constructor directly to return an instance of the SAM interface type.
  • Java’s anonymous inner classes or lambda can only capture final variables, and Kotlin can capture and modify them.
  • Kotlin’s higher-order functions can be represented by lambda, which is an implementation of the FunctionN interface.
  • Kotlin has full function types, so the SAM interface is not defined in Kotlin.
  • Lambdas are widely used in sets and sums, and the concept of inline functions was introduced to reduce the performance cost of building cars when lambdas are compiled into anonymous inner classes.
  • A lambda is a block of code. By default, the “this” inside the lambda refers to the class that contains it. By defining a lambda with recipients, you can access other objects in the lambda.