Functional programming lambda expressions in Java

catalogue

Write in front

lambda expressions and anonymous inner classes

Parameterless functional interface

Functional interface with parameters

Scope of this keyword

Syntax of lambda expressions

lambda expressions and closures

epilogue

For more technical exchanges, please search for official account number, "said the member.

Write in front

A lambda expression is an anonymous function. In Java   8, together with the functional interface, it constructs the framework of functional programming.

At first glance, lambda expressions look like a syntax sugar of anonymous inner classes, but in fact, they are two essentially different things. Anonymous inner class is essentially a class, but the programmer does not need to display the specified class name, and the compiler will automatically name the class. Lambda expression is essentially a function. Of course, the compiler will also name it. At the JVM level, the anonymous inner class corresponds to a class file, while the lambda expression corresponds to a private method of its main class.

lambda expressions can reference external variables in the function body, thus realizing closures. However, Java has a final limit on variables entering closures. Of course, we can bypass this limit.

The sample code for this article is available on gitee:

Functional programming example code in Java https://gitee.com/cnmemset/javafphttps://gitee.com/cnmemset/javafp

lambda expressions and anonymous inner classes

lambda expressions can be used to simplify some Anonymous Inner Classes   Inner Classes), but it is limited to the abbreviation of functional interfaces.

Parameterless functional interface

Take the most commonly used Runnable interface as an example:

In Java   In 7, if you need to create a new thread, the anonymous inner class is written as follows:

public static void createThreadWithAnonymousClass() {

    // Runnable is the interface name. We construct an instance of runnable by anonymous inner classes.

    Thread t = new Thread(new Runnable() {

        @Override

        public void run() {

            System.out.println("Thread is running");

        }

    });

    t.start();

}

An important purpose of using anonymous internal classes is to reduce the code burden of programmers. There is no need to define another class. Moreover, this class is a one-time class and has little reuse value. However, we will find that this object also seems redundant, because we actually don't want to pass in an object, but just want to pass in a method.

In Java   8, because   Runnable   The interface is a functional interface (interfaces with only one abstract method belong to functional interfaces). Therefore, we can use lambda expressions to simplify the writing of anonymous inner classes:

public static void createThreadWithLambda() {

    // In Java 8, Runnable is a functional interface, so we can use lambda expressions to implement it.

    Thread t = new Thread(() -> {

        System.out.println("Thread is running");

    });

    t.start();

}

Functional interface with parameters

Runnable is a parameterless functional interface. Let's look at a typical functional interface with parameters   Comparator:

@FunctionalInterface

public interface Comparator {

    int compare(T o1, T o2);

  
    ....

}

Suppose a scenario: given a phonetic list of provinces, you need to sort the provinces in the list. The sorting rule is that the provinces with the smallest letter length are ranked first. If the letter lengths of the two provinces are the same, they are sorted alphabetically.

The example code for using anonymous inner classes is as follows:

public static void sortProvincesWithAnonymousClass() {

    List list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu", "Xizang", "Fujian", "Hunan", "Guangxi");

    list.sort(new Comparator() {

        @Override

        public int compare(String first, String second) {

            int lenDiff = first.length() - second.length();

            return lenDiff == 0 ? first.compareTo(second) : lenDiff;

        }

    });

    list.forEach(s -> System.out.println(s));

}

The above code output is:

Hunan

Fujian

Xizang

Guangxi

Jiangsu

Zhejiang

Guangdong

Use lambda expression to simplify the implementation of Comparator. The example code is as follows:

public static void sortProvincesWithLambda() {

    List list = Arrays.asList("Guangdong", "Zhejiang", "Jiangsu", "Xizang", "Fujian", "Hunan", "Guangxi");

    // The following parameter lists, first and second, are the parameter lists of the method Comparator.compare

    list.sort((first, second) -> {

        int lenDiff = first.length() - second.length();

        return lenDiff == 0 ? first.compareTo(second) : lenDiff;

    });

    list.forEach(s -> System.out.println(s));

}

Note that lambda expressions with parameters do not even need to declare types, because the compiler can infer the types of parameters from the context. Of course, we can also explicitly specify the parameter type, especially when the parameter type inference fails:

(String first, String second) -> {

    int lenDiff = first.length() - second.length();

    return lenDiff == 0 ? first.compareTo(second) : lenDiff;

}

Scope of this keyword

As mentioned earlier, anonymous inner class and lambda expression are different in nature: anonymous inner class is a class in nature, while lambda expression is a function in nature. At the JVM level, the anonymous inner class corresponds to a class file, while the lambda expression corresponds to a private method of its main class.

This makes the this keyword different in anonymous inner classes and lambda expressions. In an anonymous inner class, the this keyword points to an instance of the anonymous inner class, while in a lambda expression, the this keyword points to an instance of the main class.

Let's verify with code:

public class ThisScopeExample {
    public static void main(String[] args) {
        ThisScopeExample example = new ThisScopeExample();

        // Output "I am Anonymous Class."
        // Output "I am ThisScopeExample Class."
        example.runWithAnonymousClass();
        example.runWithLambda();
    }

    public void runWithAnonymousClass() {
        // Run as an anonymous class
        run(new Runnable() {
            @Override
            public void run() {
                // this is an instance of an anonymous inner class that implements the interface Runnable
                System.out.println(this);
            }

            @Override
            public String toString() {
                return "I am Anonymous Class.";
            }
        });
    }

    public void runWithLambda() {
        // Run as a lambda expression
        run(() -> {
            // this is an instance of the class ThisScopeExample
            System.out.println(this);
        });
    }

    public void run(Runnable runnable) {
        runnable.run();
    }

    @Override
    public String toString() {
        return "I am ThisScopeExample Class.";
    }
}

The above code output is:

I am Anonymous Class.

I am ThisScopeExample Class.

Syntax of lambda expressions

The syntax of lambda expressions is: parameters, arrows (- >), and method bodies.

If the method body cannot be completed with an expression, you can put the code in curly braces like writing an ordinary method   In {}. Conversely, if the method body has only one expression, you can omit braces   { }.

For example:

(String first, String second) -> {
    int lenDiff = first.length() - second.length();
    return lenDiff == 0 ? first.compareTo(second) : lenDiff;
}

The above is a typical and complete lambda expression.

For lambda expressions without parameters, the parameter part cannot be omitted, and empty parentheses need to be provided, for example:

Supplier<Integer> supplier = () -> {
    return new Random().nextInt(100);
}

For the above lambda expression, you can find that its method body has only one expression, so it can omit braces, and even the return keyword, because the compiler can infer whether to return a value according to the context: if necessary, return the return value of the unique expression; if not, return directly after the unique expression. For example:

// Supplier < integer > needs to return a value, so the following lambda expression is equivalent to:
// () -> { return new Random().nextInt(100); }
Supplier<Integer> supplier = () -> new Random().nextInt(100);
 
// Runnable does not need to return a value, so the following lambda expression is equivalent to:
// () -> { new Random().nextInt(100); return; }
Runnable runnable = () -> new Random().nextInt(100);

If the compiler can infer the parameter type of a lambda expression, its type can be ignored:

// Here, the compiler can infer that the types of first and second are String.
Comparator<String> comp = (first, second) -> {
    int lenDiff = first.length() - second.length();
    return lenDiff == 0 ? first.compareTo(second) : lenDiff;
};

If the lambda expression has only one parameter, the parentheses in the parameter list can also be omitted:

// The value here is equal to (value)
Consumer consumer = value -> System.out.println(value);

Unlike ordinary functions, lambda expressions do not need to specify the return type. It is always inferred by the compiler. If inference fails, it defaults to Object type.

lambda expressions and closures

First of all, we should understand that lambda expression and closure are two different concepts, but they are closely related. In the case of not pursuing conceptual accuracy, it can even be said that lambda expressions in Java are closures.

Closures are also called function closures   Closure) is a technology to extend the life cycle of variables. In this sense, the functions of closure and object-oriented implementation are equivalent.

The definition of closure is: when creating or defining a function, in addition to recording the function itself, it also records the free variables (free variables) that can be accessed when creating the function   Variable refers to the variable defined outside the function, which is neither a parameter of the function nor a local variable within the function). In this way, the variable scope of the closure includes not only the local variable field of the function runtime, but also the external variable field of the function definition.

Text expression may not be intuitive enough. Let's take a look at a code example:

public class ClosureExample {
    public static void main(String[] args) {
        // square
        IntUnaryOperator square = getPowOperator(2);

        // cube
        IntUnaryOperator cube = getPowOperator(3);

        // Fourth power
        IntUnaryOperator fourthPower = getPowOperator(4);

        // Square of 5
        System.out.println(square.applyAsInt(5));
        // Cube of 5
        System.out.println(cube.applyAsInt(5));
        // The fourth power of 5
        System.out.println(fourthPower.applyAsInt(5));
    }

    public static IntUnaryOperator getPowOperator(int exp) {
        return base -> {
            // The variable exp is a parameter of getPowOperator and is a free variable when defining a lambda expression,
            // Its life cycle is extended to be as long as the returned lambda expression.
            return (int) Math.pow(base, exp);
        };
    }
}

The output of the above code is:

25

125

625

As you can see, exp is the parameter of the method getPowOperator, but it "escapes" out of the scope of getPowOperator through closure technology.

Obviously, variable "escape" can easily lead to thread safety problems in a multi-threaded environment. Therefore, Java stipulates that if an external variable is referenced inside a lambda expression, it must be final, that is, an immutable object, which can only be assigned once and cannot be modified.

(as an aside, not all languages require closures, such as Python and JavaScript. The external variables referenced in closures can be modified at will.)

For the convenience of writing code, Java   8 does not require the variable to be declared final explicitly, but if you try to modify the value of the variable, the compiler will report an error. For example:

public static IntUnaryOperator getPowOperator(int exp) {
    // Try to modify the value of exp, but the compiler will report an error in the lambda expression
    exp++;
    return base -> {
        // If you try to modify the value of exp, an error will be reported here:
        // Error: local variable referenced from lambda expression must be final variable or actual final variable
        return (int) Math.pow(base, exp);
    };
}

But this limitation is also limited, because we can modify the value of a variable by declaring it as an array or a class. For example:

public static IntUnaryOperator getPowOperator(int[] exp) {
    // Exp is an int array: exp = new int[1];
    exp[0]++;
    return base -> {
        // No error will be reported at this time, and it can operate normally
        return (int) Math.pow(base, exp[0]);
    };
}

epilogue

The emergence of lambda expressions not only supports functional programming, but also improves the productivity of Java programmers. We should be familiar with common functional interfaces and use lambda expressions and closures flexibly.

Tags: Java Lambda Back-end Functional Programming

Posted on Thu, 21 Oct 2021 02:03:26 -0400 by brbsta