Lamda expression is very convenient. It is generally used in stream programming in projects.
List<Student> studentList = gen(); Map<String, Student> map = studentList .stream() .collect(Collectors.toMap(Student::getId, a -> a, (a, b) -> a));
There are three steps to understand a Lamda expression:
- Confirm the type of Lamda expression
- Find the method to implement
- Implement this method
Confirm the type of Lamda expression
The type that can be represented by Lamda expression must be a functional interface, which is an interface with only one abstract method.
Let's take a look at the familiar Runnable interface in JDK.
@FunctionalInterface public interface Runnable { public abstract void run(); }
This is a standard functional interface. Because there is only one abstract method. And there is an annotation @ functional interface on this interface
This is only to help you check whether your interface meets the conditions of functional interface during compilation. For example, if you do not have any abstract methods or have multiple abstract methods, compilation cannot pass.
// There is no interface that implements any abstract methods @FunctionalInterface public interface MyRunnable {} // After compilation, the console displays the following information Error:(3, 1) java: Unexpected @FunctionalInterface notes MyRunnable Not a function interface At interface MyRunnable Abstract method not found in
A little more complicated, default methods and static methods are allowed in the interface after Java 8, and these are not abstract methods, so they can also be added to the functional interface.
Look at an interface that you may not be familiar with and a little deja vu.
@FunctionalInterface public interface Consumer<T> { void accept(T t); default Consumer<T> andThen(Consumer<? super T> after) {...} }
Look, there is only one abstract method and one default method (the code of the method body is omitted), which does not affect that it is a functional interface. Look at a more complex, more static method, which is also a functional interface, because it still has only one abstract method. Feel it for yourself.
@FunctionalInterface public interface Predicate<T> { boolean test(T t); default Predicate<T> and(Predicate<? super T> other) {...} default Predicate<T> negate() {...} default Predicate<T> or(Predicate<? super T> other) {...} static <T> Predicate<T> isEqual(Object targetRef) {...} static <T> Predicate<T> not(Predicate<? super T> target) {...} }
Regardless of what these methods are for, these classes are everywhere in the methods designed by Stream. Let's first remember that the type required by Lamda expression is a functional interface. Only one abstract method in the functional interface is enough. The above three examples belong to functional interfaces.
Find the method to implement
Lamda expression is to implement a method. What method? Just those abstract methods in functional interfaces.
That's too simple, because functional interfaces have and only have one abstract method. Just find it. We try to find the abstract methods of those functional interfaces.
@FunctionalInterface public interface Runnable { public abstract void run(); } @FunctionalInterface public interface Consumer<T> { void accept(T t); default Consumer<T> andThen(Consumer<? super T> after) {...} } @FunctionalInterface public interface Predicate<T> { boolean test(T t); default Predicate<T> and(Predicate<? super T> other) {...} default Predicate<T> negate() {...} default Predicate<T> or(Predicate<? super T> other) {...} static <T> Predicate<T> isEqual(Object targetRef) {...} static <T> Predicate<T> not(Predicate<? super T> target) {...} }
Implement this method
Lamda expression is to implement this abstract method. If you don't use lamda expression, you must know how to implement it with anonymous classes? For example, we just implemented the anonymous class of the predict interface.
Predicate<String> predicate = new Predicate<String>() { @Override public boolean test(String s) { return s.length() != 0; } };
What if you replace it with Lamda expression? Like this.
Predicate<String> predicate = (String s) -> { return s.length() != 0; };
This Lamda syntax consists of three parts:
- Parameter block: the previous (String s) simply writes the parameters of the abstract method to be implemented intact.
- Small arrow: that's the symbol.
- Code block: the method to be implemented is written here intact.
First, look at the fast part of the parameter. The type information in (String s) is redundant, because it can be deduced by the compiler and removed.
Predicate<String> predicate = (s) -> { return s.length() != 0; };
Parentheses can also be removed when there is only one parameter.
Predicate<String> predicate = s -> { return s.length() != 0; };
Look at the code block. There is only one line of code in the method body. You can remove the curly braces and the return keyword.
Predicate<String> p = s -> s.length() != 0;
Let's implement a Runnable interface.
@FunctionalInterface public interface Runnable { public abstract void run(); } Runnable r = () -> System.out.println("I am running");
You see, this method has no arguments, so there are no parameters in the front parentheses. In this case, the parentheses cannot be omitted.
Usually, when we quickly create a new thread and start it, is it as follows? Are you familiar with it?
new Thread(() -> System.out.println("I am running")).start();
Multiple input parameters
Before, we only tried one input parameter. Next, let's look at the of multiple input parameters.
@FunctionalInterface public interface BiConsumer<T, U> { void accept(T t, U u); // default methods removed }
Then see if a usage is clear at a glance.
BiConsumer<Random, Integer> randomNumberPrinter = (random, number) -> { for (int i = 0; i < number; i++) { System.out.println("next random = " + random.nextInt()); } }; randomNumberPrinter.accept(new Random(314L), 5));
Just a few input parameters, let's add a return value.
@FunctionalInterface public interface BiFunction<T, U, R> { R apply(T t, U u); // default methods removed } // Look at an example BiFunction<String, String, Integer> findWordInSentence = (word, sentence) -> sentence.indexOf(word);
In fact, the abstract method in a functional interface is nothing more than the number of input parameters and the type of return value. The number of input parameters can be one or two, and the return value can be void, boolean, or a type. The arrangement and combination of these cases are the classes under the java.util.function package provided by JDK.
```bash BiConsumer BiFunction BinaryOperator BiPredicate BooleanSupplier Consumer DoubleBinaryOperator DoubleConsumer DoubleFunction DoublePredicate DoubleSupplier DoubleToIntFunction DoubleToLongFunction DoubleUnaryOperator Function IntBinaryOperator IntConsumer IntFunction IntPredicate IntSupplier IntToDoubleFunction IntToLongFunction IntUnaryOperator LongBinaryOperator LongConsumer LongFunction LongPredicate LongSupplier LongToDoubleFunction LongToIntFunction LongUnaryOperator ObjDoubleConsumer ObjIntConsumer ObjLongConsumer Predicate Supplier ToDoubleBiFunction ToDoubleFunction ToIntBiFunction ToIntFunction ToLongBiFunction ToLongFunction UnaryOperator
Don't look dizzy. Let's just classify. It can be noted that many class prefixes are Int, Long and Double, which actually specifies the specific type of input parameter, rather than a generic type that can be customized by users, such as DoubleFunction.
@FunctionalInterface public interface DoubleFunction<R> { R apply(double value); }
This can be fully realized by the more free functional interface Function.
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
Then we might as well remove these specific types of functional interfaces (I also secretly removed several classes of XXXOperator because they all inherit other functional interfaces), and then sort to see what's left.
Consumer Function Predicate BiConsumer BiFunction BiPredicate Supplier
Almost none. Let's focus on these next. Here I just list the classes and corresponding abstract methods
Consumer: void accept(T t) Function: R apply(T t) Predicate: boolean test(T t) BiConsumer: void accept(T t, U u) BiFunction: R apply(T t, U u) BiPredicate: boolean test(T t, U u) Supplier: T get()
See the pattern? The above simple categories are:
supplier: No input parameter, return value. consumer: There are input parameters and no return value. predicate: With input parameters, return boolean value function: There are input parameters and return values
Then, those with Bi prefix have two input parameters, and those without Bi prefix have only one such parameter. OK, these have been clearly divided by us. In fact, it provides us with a function template. The difference is only the arrangement and combination of the number of input and return parameters.
Get familiar with our common Stream programming
If you use stream programming in your project, you must be familiar with the following code. There is a student list. You want to convert it into a map. key is the id of the student object and value is the student object itself.
List<Student> studentList = gen(); Map<String, Student> map = studentList .stream() .collect(Collectors.toMap(Student::getId, a -> a, (a, b) -> a));
Extract the part of Lamda expression.
Collectors.toMap(Student::getId, a -> a, (a, b) -> a)
Since we haven't seen this form yet, call it back as it is. Here's just for you to warm up.
Collectors.toMap(a -> a.getId(), a -> a, (a, b) -> a)
Why is it written like this? Let's look at the definition of Collectors.toMap.
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap( Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) { return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); }
There are three input parameters: Function, Function and BinaryOperator. BinaryOperator only inherits BiFunction and extends several methods. We don't use it, so we might as well treat it as BiFunction.
Remember Function and BiFunction?
Function R apply(T t) BiFunction R apply(T t, U u)
The first parameter a - > a.getId() is the implementation of R apply(T t). The input parameter is Student type object a, which returns a.getId()
The second parameter a - > A is also the implementation of R apply(T t). The input parameter is a of Student type and returns a itself
The third parameter (a, b) - > A is the implementation of R apply (T, t, u, U), the input parameters are a and B of Student type, and the return is the first input parameter a, which is used in the Stream. When the key s of two objects a and B are the same, the value takes the first element a
The second parameter a - > A in the Stream represents the value value when converting from list to map, using the original object itself. You must have seen such a writing.
Collectors.toMap(a -> a.getId(), Function.identity(), (a, b) -> a)
Why can I write like this? I'll show you the full picture of the Function class, and you'll understand.
@FunctionalInterface public interface Function<T, R> { R apply(T t); ... static <T> Function<T, T> identity() { return t -> t; } }
See, the identity method is to help us implement the expression, so we don't have to write it ourselves. In fact, it packages a method. This time I know a functional interface. Why do many of them contain a pile of default methods and static methods? It's for this.
Let's try again. There is such a default method in predict.
@FunctionalInterface public interface Predicate<T> { boolean test(T t); default Predicate<T> and(Predicate<? super T> other) { Objects.requireNonNull(other); return (t) -> test(t) && other.test(t); } }
What can it do? Let me tell you, without this method, there is a piece of code that you might write like this.
Predicate<String> p = s -> (s != null) && !s.isEmpty() && s.length() < 5;