This is the second day of my participation in the November Gwen Challenge. Check out the details: the last Gwen Challenge 2021

Read the book “Java in Action”, this article is the conclusion of chapter 3, mainly systematically combing the context of Lambda expressions, and how to use them flexibly.

Lambda syntax

1.1 features

As a succinct transitive anonymous function, it has no noun, but it has parameter list, function body and return type. It mainly has the following characteristics:

  • Anonymous: No declaration name.
  • Function: A function that does not belong to a class like a method, but, like a method, has an argument list, a function body, and a return type.
  • Pass: Lambda expressions can be passed as arguments to methods or stored in variables.
  • Brevity: Less structural template code than anonymous inner classes.

1.2 Grammatical Structure

A Lambda expression consists of three main parts: a parameter list, an arrow, and a Lambda body.

  • Parameter list: Can be multiple or no parameter.
  • Arrow: Used to separate the argument list from the Lambda body.
  • Lambda body: Generally divided into expression style (single line of code) and block style (multiple lines).

Common examples:

// Takes a String, which returns an int(String s) -> s.length();// An object type parameter, return Boolean(Apple a) -> a.getweight ()>150;
// Returns an object with no arguments() - >new Apple(10);
// No return value, consumption object, block style body(Apple a) -> {system.out.println (a.gettweigth ()); }// Two arguments, return int, object comparison(Apple A1,Apple A2) -> a1.getweight ().compareto (a2.getweight ());Copy the code

2. How to use Lambda

Where Lambda is used and how Lambda is used correspond to two concepts, functional interfaces + function descriptors.

2.1 Functional interface

  • Define an interface for only one abstract method.
  • Lambda essentially provides implementations of abstract methods of functional interfaces in inline form, a complete expression == a concrete implementation instance of a functional interface. Therefore, methods or arguments that have functional interfaces can be implemented using lambda.

2.2 Function descriptors

Functional interfaces are the basis for Lambda, and how and how they are used is determined by their internal abstract method signatures (method parameters + return values). Therefore, the abstract methods of functional interfaces, function descriptors, are also signatures of Lambda expressions.

2.3 @ FunctionalInterface annotation

This annotation is recommended for functional interfaces, mainly to declare the interface as a functional interface. The compiler checks if the interface has and only declares one abstract method.

Lambda combat

Surrounding the execution mode is the production of common scenarios, such as resource processing ‘open a resources > do some processing > close resources’, which is always similar to open and close, and dealing with the middle there is a changeable, very suitable for Lambda expressions, so we are in order, for example, demonstrates how to turn a traditional method is extended to support variable Lambda scene:

    // Read a line of data from a file to demonstrate how a traditional process can be modified to support lambdas
	// Traditional implementation: fixed write dead logic
    public static String processFile(a) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(filePath))){
            returnbr.readLine(); }}// Based on Lambda extensions
    //1.
    //1.1 introduces functional interfaces
    @FunctionalInterface
    interface ProcessFilePredicate{
        String process(BufferedReader br) throws IOException ;
    }
    //1.2 Traditional behavior with functional interfaces
    public static String processFile(ProcessFilePredicate p) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(filePath))){
            returnp.process(br); }}public static void main(String[] args) throws IOException {
        System.out.println(processFile());
        System.out.println("-");
        // Flexible use based on Lambda
        String oneLine = processFile((BufferedReader br) -> br.readLine());
        System.out.println(oneLine);
        System.out.println("-");
        String twoLine = processFile((BufferedReader br) -> br.readLine()+br.readLine());
        System.out.println(twoLine);
    }
Copy the code

The overall core element is that the method realizes behavior parameterization through corresponding functional interface, and then realizes corresponding behavior definition quickly through Lambda expression as needed, complete code address.

4. Java8 has its own functional interface

In order to apply the needs of different Lambda expressions, Java8 has defined some common functional interface implementations under java.util.function, which fall into three main categories:

  • Four common types:Predicate<T>,Consumer<T>,Function<T,R>,Supplier<T>
  • Base type specialization:IntPredicate,LongPredicateSuch as processing specific specific basic data type parameters interface, avoid data type unpacking, packing loss
  • Extension of two parameters:BiPredicate<T,U>,BiConsumer<T,U>,BiFunction<T,U,R>

4.1 Predicate

  • Abstract method: Boolean test(T T), which takes a generic argument and returns Boolean.

  • Function descriptors: T -> Boolean

  • Application scenario: Represents a Boolean expression involving type T

    // A Boolean function with one argument
    @FunctionalInterface
    public interface Predicate<T> { 
        boolean test(T t);
    }
    Copy the code

4.2 Consumer

  • Abstract method:void accept(T t)Accepts a generic parameter with no return value.
  • Function descriptor:T -> void.
  • Application scenario: Access objects of the generic T and perform some operations on them without returning.

4.3 the Function < R > T,

  • Abstract method:R apply(T t)Accepts an object of generic T and returns an object of generic R.
  • Function descriptor:T -> R.
  • Application scenario: Input objects map output to new objects.

5. Underlying mechanism and rules of Lambda

I’ve seen how to use it and how to use it, but I’ll take a closer look at how the compiler handles it and some of the existing rules.

The expression of Lambda is equivalent to an instance of a functional interface, but it does not contain specific interface information. Therefore, the required type, also known as the target type, needs to be inferred from the context. There are three main inference methods:

  • Method call context (parameters and return values)
  • The context of the assignment
  • The context of the conversion

5.1 Type Check

Main work contents of type check:

  • Find the target type by calling the method context match.
  • The corresponding function descriptor is inferred by the abstract method of the target type.
  • Parse the Lambda expression signature and check for matches with the function descriptor of the target type.

The detailed process example is shown below:

5.2 Similarly Lambda different functional interfaces

With the concept of target type, the same expression can be assigned to different functional interfaces, as long as the expressions are compatible. As follows:

// Example 1:
Callable<Integer> c = () -> 42; // Assign target object Callable
      
PrivilegedAction<Integer> p = () -> 42;// Assign a PrivilegedAction
      
        to the target object
      
// Example 2:
Comparator<Apple> c1 = 
 	(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 
ToIntBiFunction<Apple, Apple> c2 = 
 	(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 
BiFunction<Apple, Apple, Integer> c3 = 
	 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Copy the code

5.3 Parameter Type Inference

The compiler will infer from the context (the target type) that the functional interface matches the Lambda expression, which means that the signature of the Lambda can be inferred, so the function descriptor can be obtained from the target type, thus further omitting the type, as shown in the following example:

// Original format
Comparator<Apple> c = 
 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); 
// Do not declare the type, the compiler will automatically infer
Comparator<Apple> c = 
 (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
Copy the code

5.4 Restrictions on the use of local variables

Lambda expressions are similar in nature to anonymous functions. Therefore, when using external variables, the type of the external variable must be final or actually final. Otherwise, an error will be reported.

The essential reasons for this restriction:

  • Instance variables are stored in the heap and local variables are kept on the stack. Lambda uses a copy of it, not the original, and the change is invisible and leads to misunderstanding, so the change is forbidden directly (lambda is closed to values, not variables).
  • The typical imperative programming pattern of changing external variables is discouraged.

6. Method references

6.1 define

Reuse method definitions and pass them like a Lambda.

  • Basic idea: A Lambda simply describes’ calling this method directly ‘, preferably by name rather than how to call it, since the former is more readable.
  • Base format: Target reference puts delimiter: :Before, the method name comes after.

6.2 Constructing a Scenario

There are three main types of method references:

  • Method references to static methods: such as Integer’s parseInt method, writtenInteger::parseInt
  • Instance method references of any type: for example, the length method of String, writingString::length
  • Instance methods that point to existing objects: such as the getValue method of the local variable expexp::getValue

In addition, there are special forms of method references for constructors, array constructors, and super-calls.

// Example 1:
// Lambda representation(args) -> classaname. staticMethod(args)// The corresponding method is written by reference
 ClassName::staticMethod
     
// Example 2:
// Lambda representation (arg0 is an instance of ClassName)(arg0,rest) -> arg0.instancemethod (rest)// The corresponding method is written by reference
 ClassName::instanceMethod
     
// Example 3:
// Lambda representation(ARgs) -> expr. InstanceMethod (args)// The corresponding method is written by reference
 expr::instanceMethod
Copy the code

6.2 Constructor references

You can choose the corresponding abstract method based on the type of the constructor.

  • No parameter constructor, consistent with the Supplier

    signature ()-> T.

    // Method reference implementation
    Supplier<Apple> c1 = Apple::new;
    Apple a1 = c1.get()
    
    // Equivalent Lambda implementation
    Supplier<Apple> c1 = () -> new Apple();
    Apple a1 = c1.get()    
        
        
    Copy the code
  • An argument, consistent with the Function

    Function descriptor T -> R.
    ,r>

    // Method reference implementation
    Function<Integer,Apple> c1 = Apple::new;
    Apple a1 = c1.apply(100);
    
    // Equivalent Lambda implementation
    Function<Integer,Apple>  c1 = (weight) -> new Apple(weight);
    Apple a1 = c1.apply(100);  
    Copy the code
  • Two parameters, consistent with BiFunction

    signature
    ,u,r>

    // Method reference implementation
    BiFunction<String,Integer,Apple> c1 = Apple::new;
    Apple a1 = c1.apply(Color.RED,100);
    
    // Equivalent Lambda implementation
    BiFunction<String,Integer,Apple>  c1 = (color,weight) -> new Apple(color,weight);
    Apple a1 = c1.apply(Color.RED,100); 
    Copy the code

Common ways to compound Lambda expressions

It is used to combine simple lambdas into complex expressions, including comparator composition, Predicate composition, and function composition.

7.1 Composition of comparators

  • The reverse

    inventory.sort(comparing(Apple::getWeight).reversed());
    Copy the code
  • Comparator chain: Mainly used to solve multi-level comparison, such as apples sorted by decreasing weight, same weight, and then sorted by country

    inventory.sort(comparing(Apple::getWeight)
        .reversed()
        .thenComparing(Apple::getCountry)
    );
    Copy the code

7.2 Predicate composite

There are mainly negate- not, and- and, or- or three compound operations.

// Red apple
Predicate<Apple> redApple = (a) -> Color.RED.equals(a.getColor());
// Non-red apples
Predicate<Apple> notRedApple = redApple.negate();
// Red and weighs more than 150
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
// Red or green apples with weight >150
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150) 
 .or(a -> "green".equals(a.getColor()));
Copy the code

7.3 the Function of composite

The Function interface configures the andThen and compose default methods to compose them.

  • AndThen: a function that evaluates itself first andThen its parameters
  • Compose: Evaluates the parameter function first, the result as a parameter, and then evaluates itself
        Function<Integer,Integer> f = (x) -> x+1;
        Function<Integer,Integer> g = (x) -> x*2;

        / / andThen test
        Function<Integer,Integer> h1 = f.andThen(g);
        // result: 4 (f +1, g *2)
        System.out.println(h1.apply(1));

        / / compose test
        Function<Integer,Integer> h2 = f.compose(g);
        // 结果:3 (先执行g的*2,再执行f的+1)
        System.out.println(h2.apply(1));
Copy the code