New feature of JDK8 -- Lambda expression

Posted by ShadowX on Mon, 24 Jan 2022 14:16:14 +0100

Brief introduction to Lambda

Source: the eleventh letter of the Greek alphabet( λ), The English name is Lambda.

Function: avoid too many anonymous inner class definitions.

Essence: it belongs to the concept of functional programming.

Why use lambda expressions?

  1. Avoid too many anonymous inner class definitions
  2. It can make the code look concise
  3. Removed a pile of meaningless code, leaving only the core logic
  4. So that we can focus more on What rather than How

Understanding = = functional interface = = is the key to learning java 8 lambda expressions.

Definition of functional interface: it refers to an interface that has only one abstract method, but can have multiple non abstract methods (such as static methods). For functional interfaces, we can create objects of the interface through lambda expressions.

lambda expression: the embodiment of functional programming.

@Functional interface annotation

Java 8 introduces a new annotation for functional interfaces: @ functional interface.
This annotation can be used for the definition of an interface:

@FunctionalInterface
public interface InterfaceA {
void methodA();
}

Once the annotation is used to define the interface, the compiler will force to check whether the interface does have and only has one abstract method, otherwise an error will be reported. It should be noted that even if the annotation is not used, as long as the definition of the functional interface is met, it is still a functional interface, which is the same.

Common functional interfaces

  1. Supplier interface

    java. util. function. The supplier interface contains only one parameterless method: T get() (used to get object data of a specified generic type). Because this is a functional interface, it means that the corresponding Lambda expression needs to "provide" an object data conforming to the generic type.

    public class SupplierDemo {
        public static void main(String[] args) {
        	getString(() -> "Hello World");
        }
        
        public static String getString(Supplier<String> supplier) {
        	return supplier.get();
        }
    }
    

    Practice using the Supplier interface as the method parameter type to find the maximum value in the array through Lambda expression, as shown below:

    public class SupplierDemo {
        public static void main(String[] args) {
            int[] arr = {11, 8, 21, 88, 21, 66};
            int maxNum = getMax(() -> {
                int max = arr[0];
                for (int i : arr) {
                    if (i > max) {
                        max = i;
                    }
                }
                return max;
            });
            System.out.println("The maximum value is:"+maxNum);
        }
    
        public static Integer getMax(Supplier<Integer> supplier) {
            return supplier.get();
        }
    }
    

    Output result:

    Maximum: 88
    
  2. Consumer interface

    java. util. function. The consumer interface is just opposite to the Supplier interface. Instead of producing one data, it consumes one data,
    Its data type is determined by generics.
    The Consumer interface contains the abstract method void accept (T) (consuming data of a specified generic type).

    public class ConsumerDemo {
        public static void main(String[] args) {
            testConsumer((str) -> {
            	System.out.println(str);
            });
        }
        public static void testConsumer (Consumer<String> consumer) {
        	consumer.accept("Hello World");
        }
    }
    

    Practice using the Consumer parameter to output a sentence, as follows:

    public class ConsumerDemo {
        public static void main(String[] args) {
            testConsumer(str -> {
                System.out.println("Output a sentence:"+str);
            });
        }
    
        public static void testConsumer(Consumer<String> consumer) {
            consumer.accept("Hello World~");
        }
    }
    

    Output result:

    Output a sentence: Hello World~
    

    Default method: andThen()
    If the parameters and return values of a method are all of Consumer type, the effect can be achieved: when consuming data, first do an operation, and then do an operation to realize combination. In fact, there is such a method in the Consumer interface, which is the default method andThen() in the Consumer interface. The following is the source code of JDK:

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
    

    Source code analysis: if there are objects a and B of Consumer type, when calling the andThen method of a, the B object is passed in as a parameter, and a new Consumer object C is returned. The implementation of the accept method of the C object can be analyzed through the source code. A.andThen(B), when calling the andThen method, the current object is a, and the specific implementation is the accept method of C, The first line of the accept method is the accept method of the current object. The current object is a, so the accept method of the a object is executed. The second line of the accept method of after is executed, and after is the incoming B object.

    public class ConsumerDemo {
        public static void main(String[] args) {
            testConsumerAndThen(
                str -> System.out.println(str.toUpperCase()),
                str -> System.out.println(str.toLowerCase())
            );
        }
        
        public static void testConsumerAndThen (Consumer<String> conA, Consumer<String> conB) {
            // The andThen method returns a new Consumer object as conC, which executes the accept method of conC
            conA.andThen(conB).accept("Hello");
        }
    }
    

    Program parsing: when the main method calls the testConsumerAndThen method, two Consumer objects, an A and a B, are passed in. When a calls its own andThen method, it passes in the B object as a parameter and returns a new Consumer object C. The final execution is the accept method of the C object. The accept method of C object executes the accept method of a object, and then executes the accept method of B object.

    There are multiple pieces of information in the string array below. Please follow the format "Name: XX, gender: XX." Print the information in the format of.

    public class ConsumerDemo2 {
        public static void main(String[] args) {
            String[] arr = {"Wang Erni,female","Zhang San,male","Li Si,male"};
            testConsumer(
                    str -> System.out.print("full name:"+str.split(",")[0]+","),
                    str -> System.out.println("Gender:"+str.split(",")[1]+". "),
                    arr);
        }
    
        public static void testConsumer(Consumer<String> cs1, Consumer<String> cs2, String[] arr) {
            for (String str : arr) {
                cs1.andThen(cs2).accept(str);
            }
        }
    }
    

    Output result:

    Name: Wang Erni, gender: female.
    Name: Zhang San, gender: male.
    Name: Li Si, gender: male.
    
  3. Predict interface

    Sometimes we need to judge some type of data to get a boolean value result. You can use Java util. function. Predict interface.

    Abstract method: test()
    The predict interface contains an abstract method: Boolean test (T). Scenario for condition judgment.

    public class PredicateDemo {
        public static void main(String[] args) {
            testPredicate(str -> str.length() > 6);
        }
        public static void testPredicate(Predicate<String> predicate) {
            boolean flag = predicate.test("HelloWorld");
            System.out.println("Characters longer than 6:"+flag);
        }
    }
    

    Output result:

    Characters longer than 6: true
    
  4. Function interface
    java. util. function. The function < T, R > interface is used to obtain another type of data from one type of data. The former is called pre condition and the latter is called post condition.
    Abstract method: apply()
    The main abstract method in the Function interface is R apply(T t), which obtains the result of type R according to the parameters of type T. Use scenarios such as converting String type to Integer type.

public class FunctionDemo {
    public static void main(String[] args) {
        int n = testFunction(str -> Integer.parseInt(str));
        System.out.println(n+12);
    }

    public static int testFunction(Function<String, Integer> function) {
        Integer num = function.apply("20");
        return num;
    }
}
Output result: 32

Default method: andThen()
There is a default andThen method in the Function interface, which is used for combination operations. JDK source code, such as:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    // andThen's method actually returns a new Function object. The implementation method is to re-enter the return value of the object calling the apply method
    // The application passing in the second Function object continues to be processed, and the final result will be returned after processing
    return (T t) -> after.apply(apply(t));
}

In the exercise, please use Function to splice the Function model. The multiple Function operations to be performed in order are: String str = "Little Red Riding Hood, 3";

  1. Intercepting the digital age part of the string to obtain the string;
  2. Convert the string in the previous step into a number of type int;
  3. Accumulate the int number of the previous step by 100 to obtain the result int number
public class FunctionTest {
    public static void main(String[] args) {
        String str = "Little Red Riding Hood,3";
        Integer n = testFunction(s -> str.split(",")[1],
                s -> Integer.parseInt(s),
                s -> s + 100, str);
        System.out.println("The result is:"+n);
    }

    public static Integer testFunction(Function<String, String> fun1,
                                    Function<String, Integer> fun2,
                                    Function<Integer, Integer> fun3,
                                    String str) {
        return fun1.andThen(fun2).andThen(fun3).apply(str);
    }
}

Output result:

The result is: 103

Stream stream

Lambda in Java 8 allows us to focus more on What rather than How.

Disadvantages of loop traversal:

  1. The syntax of the for loop is how to do it
  2. What does the for loop do

Why use loops? Because of traversal. But is loop the only way to traverse? Traversal refers to the processing of each element in the collection, not a cycle from the first to the last. The former is the purpose and the latter is the way.

Practice traversing collection elements by criteria: filter out names that start with "small" and have a length of 3. Lambda's better description of traversal:

public class StreamTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Little Red Riding Hood");
        list.add("little dragon maiden");
        list.add("Little white dragon");
        list.add("floret");
        list.add("Skin shrimp");
        list.add("Bai Xiaochun");

        list.stream().filter(str -> str.startsWith("Small")).filter(str -> str.length() == 3)
                .forEach(System.out::println);
    }
}

Output result:

Little Red Riding Hood
 little dragon maiden
 Little white dragon

Directly reading the literal meaning of the code can perfectly display the semantics of irrelevant logical methods: obtaining the stream, filtering the last name, filtering the length of 3, and printing one by one. The code does not reflect the use of linear loops or any other algorithm for traversal. What we really want to do is better reflected in the code.

Each filter returns a new Stream object, but the Stream object does not contain element information, but only contains behavior specifications, which is a bit similar to decorator syntax.

Stream flow concept

Stream stream is actually a functional model of collection elements. It is neither a collection nor a data structure. It does not store any elements.
A Stream stream is a queue of elements from a data source.
1. Elements are specific types of objects that form a queue.
2. Stream in Java does not store elements, but calculates on demand. The source of the data stream. It can be a collection, an array, etc.

Java 8 (::) method reference

Double colon:: writing, which is called "method reference", and double colon is a new syntax in Java 8.
Double colon:: is a reference operator, and its expression is called a method reference. If the function scheme to be expressed by Lambda already exists in a specific method implementation, the method can be referenced as a substitute for Lambda through double colons.

Lambda expression: S - > system out. println(s);

Method reference writing method: system out :: println

The first semantics refers to: after getting the parameters, it is passed to the system through Lambda out. Println method.
The semantics of the second equivalent writing method refers to: directly let system Out to replace Lambda. The execution effect of the two writing methods is exactly the same, while the writing method referenced by the second method reuses the existing scheme and is more concise.

Practice changing lowercase letters to uppercase letters:

public class MethodRefObject {
    public static void main(String[] args) {
        MethodRefObject obj = new MethodRefObject();
        printData(obj :: printString);
    }

    private void printString(String str) {
        System.out.println(str.toUpperCase());
    }

    private static void printData(PrintAble data) {
        data.print("hello");
    }

    // Functional interface
    @FunctionalInterface
    interface PrintAble {
        void print(String str);
    }
}

Output result: HELLO

Simplified derivation of lambda expression

public class TestLambda {
    // 2. Static inner class
    static class MyHobby2 implements Hobby {
        @Override
        public void hobby() {
            System.out.println("And listen to music~");
        }
    }

    public static void main(String[] args) {
        // 1. External class
        Hobby hobby = new MyHobby();
        hobby.hobby();

        hobby = new MyHobby2();
        hobby.hobby();

        // 3. Local inner class
        class MyHobby3 implements Hobby {
            @Override
            public void hobby() {
                System.out.println("And to see the fields far away~");
            }
        }
        hobby = new MyHobby3();
        hobby.hobby();

        // 4. Anonymous inner class (no class name, must rely on the interface or its parent class)
        hobby = new Hobby() {
            @Override
            public void hobby() {
                System.out.println("And take some scenery photos~");
            }
        };
        hobby.hobby();

        // 5. lambda expression
        hobby = () ->System.out.println("Prefer a person in a daze~");
        hobby.hobby();
    }
}

// Define a functional interface
interface Hobby {
    void hobby();
}

// Define an implementation class to implement the Hobby interface
class MyHobby implements Hobby {
    @Override
    public void hobby() {
        System.out.println("My hobby is reading~");
    }
}

Output result:

My hobby is reading~
And listen to music~
And to see the fields far away~
And take some scenery photos~
Prefer a person in a daze~

Summary

  1. The premise of using lambda expression: it must be a functional interface (an interface with only one abstract method);
  2. lambda expressions can only be reduced to one line if there is only one line of code. If there are multiple lines, you need to wrap them with code blocks;
  3. The data type of the parameter list of Lambda expression can be omitted, because the JVM compiler infers the data type through the context, that is, "type inference". You can also remove parameter types from multiple parameters. To remove them, you need to remove them all. Parentheses must be added when multiple parameters are used.

Topics: Java Lambda stream