Java 8 Lambda expression tutorial
1. What is a Lambda expression?
Lambda expressions are essentially an anonymous method. Let's take a look at the following example:
Public int add (int x, int y ){
Return x + y;
}
After converting it to a Lambda expression, it looks like this:
(Int x, int y)-> x + y;
The parameter type can also be omitted. the Java compiler will deduce the parameter type based on the context:
(X, y)-> x + y; // returns the sum of the two numbers.
Or
(X, y)-> {return x + y;} // explicitly specify the return value
The Lambda expression consists of three parts: the parameter list, the arrow (->), and an expression or statement block.
In the following example, the lambda expression has no parameters and no return value (equivalent to a method that accepts 0 parameters and returns void, which is actually an implementation of the run method in Runnable ):
()-> {System. out. println ("Hello Lambda! ");}
If there is only one parameter and the type can be inferred by Java, the brackets in the parameter list can also be omitted:
C-> {return c. size ();}
2. Type of the lambda expression (is it an Object ?)
Lambda expressions can be considered as an Object (note the wording ). The type of the lambda expression is called "target type )". The target type of the lambda expression is "function interface", which is a new concept introduced by Java 8. It is defined as an interface. If there is only one explicitly declared abstract method, it is a function interface. It is usually marked with @ FunctionalInterface (or not marked ). Example:
@ FunctionalInterface
Public interface Runnable {void run ();}
Public interface Callable {V call () throws Exception ;}
Public interface ActionListener {void actionreceivmed (ActionEvent e );}
Public interface Comparator {Int compare (T o1, T o2); boolean equals (Object obj );}
Note the Comparator interface. It declares two methods and does not seem to comply with the definition of the function interface, but it is indeed a function interface. This is because the equals method is Object, and all interfaces declare the public method of the Object-although mostly implicit. Therefore, Comparator explicitly declares that equals does not affect the function interface.
You can use a Lambda expression to assign a value to a function interface:
Runnable r1 = ()-> {System. out. println ("Hello Lambda! ");};
Then assign the value to an Object:
Object obj = r1;
But it cannot do this:
Object obj = ()-> {System. out. println ("Hello Lambda! ") ;}; // ERROR! Object is not a functional interface!
It must be converted to a function interface explicitly:
Object o = (Runnable) ()-> {System. out. println ("hi") ;}; // correct
A Lambda expression can be used as an Object only after being transformed into a function interface. Therefore, the following sentence cannot be compiled:
System. out. println ()->{}); // error! The target type is unknown.
Transformation is required first:
System. out. println (Runnable) ()->{}); // correct
Assume that you have written a function interface, which is exactly the same as Runnable:
@ FunctionalInterface
Public interface MyRunnable {
Public void run ();
}
So
Runnable r1 = ()-> {System. out. println ("Hello Lambda! ");};
MyRunnable2 r2 = ()-> {System. out. println ("Hello Lambda! ");};
Are all correct statements. This indicates that a Lambda expression can have multiple target types (function interfaces), as long as the function matches successfully.
Note that a Lambda expression must have at least one target type.
JDK predefines many function interfaces to avoid repeated definitions. The most typical Function is:
@ FunctionalInterface
Public interface Function {
R apply (T t );
}
This interface represents a function that accepts a T-type parameter and returns an R-type return value.
Another predefined Function interface is called Consumer. The only difference with Function is that it does not return values.
@ FunctionalInterface
Public interface Consumer {
Void accept (T t );
}
Another Predicate is used to determine whether a condition is met. It is often used for filtering:
@ FunctionalInterface
Public interface Predicate {
Boolean test (T t );
}
To sum up, a Lambda expression actually defines an anonymous method, but this method must comply with at least one function interface.
3. Use of lambda expressions
3.1 where is the lambda expression used?
Lambda expressions are mainly used to replace the previously widely used internal anonymous classes and various callbacks, such as the event responder and the Runnable passed into the Thread class. See the following example:
Thread oldSchool = new Thread (new Runnable (){
@ Override
Public void run (){
System. out. println ("This is from an anonymous class .");
}
});
Thread gaoDuanDaQiShangDangCi = new Thread ()-> {
System. out. println ("This is from an anonymous method (lambda exp ).");
});
Note that you do not need to explicitly convert the lambda expression in the second Thread into a Runnable, Because Java can automatically infer from the context that a Thread constructor accepts a Runnable parameter, the passed Lambda expression exactly matches its run () function, so the Java compiler deduced it as Runnable.
In terms of form, the lambda expression only saves you a few lines of code. But the motivation for introducing lambda expressions into Java is not just that. Java 8 has a short-term objective and a long-term objective. The short-term objective is to work with the internal iteration and parallel processing of "Set-class batch processing" (as described below ); the long-term goal is to direct Java to the functional programming language (not to completely become a functional programming language, but to make it more functional programming language features ), for this reason, Oracle does not simply use internal classes to implement lambda expressions, instead, it uses an invokedynamic that is more dynamic, flexible, and easy to expand and change in the future ).
3.2 lambda expressions and set-class batch processing operations (or block operations)
The batch operation of the Collection class is mentioned above. This is another important feature of Java 8. Its cooperation with Lambda expressions is the main feature of Java 8. The batch processing API of the Collection class aims to implement the "internal iteration" of the Collection class and expects to make full use of the modern multi-core CPU for parallel computing.
Before Java 8, the Iteration of the Collection class is external, that is, the customer code. Internal iteration means that the Java class library is used for iteration instead of the Customer Code. For example:
For (Object o: list) {// external Iteration
System. out. println (o );
}
Can be written:
List. forEach (o-> {System. out. println (o) ;}); // implement internal iteration of the forEach Function
The Collection class (including List) now has a forEach method that iterates (traverses) the elements, so we do not need to write the for loop. The forEach method accepts a function interface Consumer as a parameter, so the lambda expression can be used.
This internal iteration method is widely used in various languages, such as C ++ STL Algorithm Library, python, ruby, scala, etc.
Java 8 introduces another important concept for the Collection class: stream ). A circulation usually uses a collection class instance as its data source, and then defines various operations on it. The stream API design uses the pipeline (pipelines) mode. A streaming operation returns another stream. Similar to the io api or StringBuffer append method, multiple different operations can be serialized in one statement. See the following example:
List Shapes =...
Shapes. stream ()
. Filter (s-> s. getColor () = BLUE)
. ForEach (s-> s. setColor (RED ));
First, call the stream method to generate a stream based on the elements in the shapes object of the Collection class. Then, the filter method is called on the stream to pick out the blue one and return another stream. Finally, call the forEach method to spray these blue objects in red. (The forEach method no longer returns a stream, but a terminal method, similar to the toString that StringBuffer calls after several appends)
The parameters of the filter method are of the Predicate type, while those of the forEach method are of the Consumer type. They are all function interfaces, so lambda expressions can be used.
Another method is parallelStream (). As its name suggests, it is the same as stream (), but it only specifies that parallel processing is required to fully utilize the multi-core features of modern CPU.
Shapes. parallelStream (); // or shapes. stream (). parallel ()
Let's look at more examples. The following is a typical big data processing method, Filter-Map-Reduce:
// Returns an array of the String type to identify all non-repeated prime numbers.
Public void distinctPrimary (String... numbers ){
List L = Arrays. asList (numbers );
List R = l. stream ()
. Map (e-> new Integer (e ))
. Filter (e-> Primes. isPrime (e ))
. Distinct ()
. Collect (Collectors. toList ());
System. out. println ("distinctPrimary result is:" + r );
}
Step 1: input a series of strings (assuming all valid numbers), convert them into a List, and then call stream () to generate a stream.
Step 2: Call the map method of the stream to convert each element from String to Integer to get a new stream. The map method accepts a Function-type parameter. As described above, Function is a Function interface, so the lambda expression is used here.
Step 3: Call the stream filter method to filter the numbers that are not prime numbers and obtain a new stream. The filter method accepts a parameter of the Predicate type. As described above, Predicate is a function interface, so the lambda expression is used here.
Step 4: Call the distinct method of the stream to remove duplicates and get a new stream. This is essentially another filter operation.
Step 5: Use the collect method to collect the final result into a List. The collect method accepts a Collector type parameter that specifies how the final result is collected. In this example, the results are simply collected to a List. We can also use Collectors. toMap (e-> e, e-> e) collects the results into a Map, which means that the result is received by a Map, use these prime numbers as both keys and values. The toMap method accepts two Function-type parameters, which are used to generate keys and values respectively. Function is a Function interface, so here the lambda expression is used.
In this example, List l is iterated many times, and map, filter, and distinct are all in one loop, which may be inefficient. This is not the case. The methods for returning another Stream are "lazy", while the collect Method for returning the final result is "eager. The lazy method is not executed until the eager method is encountered.
When the eager method is encountered, the preceding lazy method will be executed in sequence. The pipeline is also implemented in a consistent manner. This means that each element goes through these pipelines in sequence. For example, if there is an element "3", it is first mapped to the integer type 3; then through the filter, it is found to be a prime number, it is retained; and through distinct, if there is already a 3, it is discarded directly. If not, it is retained. In this way, three operations actually go through only one loop.
In addition to collect, other eager operations include forEach, toArray, and reduce.
The following is the most common collector method, groupingBy:
// Returns an array of the String type, finds each prime number, and counts the number of occurrences.
Public void primaryOccurrence (String... numbers ){
List L = Arrays. asList (numbers );
Map R = l. stream ()
. Map (e-> new Integer (e ))
. Filter (e-> Primes. isPrime (e ))
. Collect (Collectors. groupingBy (p-> p, Collectors. summingInt (p-> 1 )));
System. out. println ("primaryOccurrence result is:" + r );
}
Note This line:
Collectors. groupingBy (p-> p, Collectors. summingInt (p-> 1 ))
It means that the result is collected to a Map, and each Prime Number calculated is used as the key, and the number of occurrences is used as the value.
The following is an example of reduce:
// Returns an array of the String type, and calculates the sum of all non-repeated prime numbers.
Public void distinctPrimarySum (String... numbers ){
List L = Arrays. asList (numbers );
Int sum = l. stream ()
. Map (e-> new Integer (e ))
. Filter (e-> Primes. isPrime (e ))
. Distinct ()
. Reduce (0, (x, y)-> x + y); // equivalent to. sum ()
System. out. println ("distinctPrimarySum result is:" + sum );
}
The reduce method is used to generate a single final result.
A stream has many predefined reduce operations, such as sum (), max (), and min.
Another example in the real world is:
// Count the number and proportion of men and women aged between 25 and 35
Public void boysAndGirls (List Persons ){
Map Result = persons. parallelStream (). filter (p-> p. getAge () >=25 & p. getAge () <= 35 ).
Collect (
Collectors. groupingBy (p-> p. getSex (), Collectors. summingInt (p-> 1 ))
);
System. out. print ("boysAndGirls result is" + result );
System. out. println (", ratio (male: female) is" + (float) result. get (Person. MALE)/result. get (Person. FEMALE ));
}
3.3 more common lambda expressions
// Nested Lambda expression
Callable C1 = ()-> {System. out. println ("Nested lambda ");};
C1.call (). run ();
// Used in conditional expressions
Callable C2 = true? ()-> 42): ()-> 24 );
System. out. println (c2.call ());
// Define a recursive function. Note that this is required.
Protected UnaryOperator Factorial = I-> I = 0? 1: I * this. factorial. apply (I-1 );
...
System. out. println (factorial. apply (3 ));
In Java, the method with declaration and call does not work. For example, in the following example, a Lambda expression (x, y)-> x + y is declared, at the same time, an attempt is made to call it by passing in a real parameter (2, 3:
Int five = (x, y)-> x + y) (2, 3); // ERROR! Try to call a lambda in-place
This is acceptable in C ++, but not in Java. Java lambda expressions can only be used for value assignment, parameter passing, and return values.
4. Other related concepts
4.1 Capture)
The concept of capture is to solve the external variables that can be used in lambda expressions (except its own parameters and locally defined variables.
The answer is: it is very similar to the internal class, but there are differences. The difference is that the internal class always holds a reference of its external class object. Lambda expressions do not hold references to an object unless they use methods or members of an external class (surrounded class) object internally.
Before Java 8, if you want to access a local variable of an external object in the internal class, the variable must be declared as final. In Java 8, this restriction is removed, replacing it with a new concept called "tively final ". It means you can declare it as final, or do not declare final, but use it according to final, that is, a value assignment will never change. In other words, make sure that the final prefix does not cause compilation errors.
In Java 8, both internal classes and lambda expressions can access the local variable of javastively final. An example of a Lambda expression is as follows:
...
Int tmp1 = 1; // member variable of the surrounding class
Static int tmp2 = 2; // static member variable of the surrounding class
Public void testCapture (){
Int tmp3 = 3; // It is not declared as final, but the local variable of effectively final
Final int tmp4 = 4; // The local variable declared as final
Int tmp5 = 5; // common local variable
Function F1 = I-> I + tmp1;
Function F2 = I-> I + tmp2;
Function F3 = I-> I + tmp3;
Function F4 = I-> I + tmp4;
Function F5 = I-> {
Tmp5 + = I; // compilation error! Assign values to tmp5 so that it is not objective tively final
Return tmp5;
};
...
Tmp5 = 9; // compilation error! Assign values to tmp5 so that it is not objective tively final
}
...
The reason why Java requires the local variable final or javastively final is the multi-thread concurrency problem. Internal classes and lambda expressions may all be executed in different threads, allowing multiple threads to modify a local variable at the same time does not comply with Java's design philosophy.
4.2 Method reference)
Any Lambda expression can represent the anonymous descriptor of the unique method of a function interface. We can also use a specific method of a class to represent this descriptor, called method reference. For example:
Integer: parseInt // static method reference
System. out: print // instance method reference
Person: new // constructor reference
Here is a group of examples to teach you how to use method reference instead of lambda expressions:
// C1 is the same as c2 (static method reference)
Comparator C2 = (x, y)-> Integer. compare (x, y );
Comparator C1 = Integer: compare;
// The following two statements are the same (instance method reference 1)
Persons. forEach (e-> System. out. println (e ));
Persons. forEach (System. out: println );
// The following two statements are the same (instance method Reference 2)
Persons. forEach (person-> person. eat ());
Persons. forEach (Person: eat );
// The following two statements are the same (constructor reference)
StrList. stream (). map (s-> new Integer (s ));
StrList. stream (). map (Integer: new );
Use method reference to shorten your program. The distinctPrimarySum method can be rewritten as follows:
Public void distinctPrimarySum (String... numbers ){
List L = Arrays. asList (numbers );
Int sum = l. stream (). map (Integer: new). filter (Primes: isPrime). distinct (). sum ();
System. out. println ("distinctPrimarySum result is:" + sum );
}
There are also some other method references:
Super: toString // reference the parent class method of an object
String []: new // reference an array Constructor
4.3 Default method)
In Java 8, the interface declaration can be implemented by a method called the default method. Before that, all methods in the interface were abstract methods.
Public interface MyInterf {
String m1 ();
Default String m2 (){
Return "Hello default method! ";
}
}
This actually obfuscated the interface and abstract class, but a class can still implement multiple interfaces, and can only inherit one abstract class.
This is because the Collection library needs to add new methods for batch operations, such as forEach () and stream, but you cannot modify the existing Collection interface. If you do that, all the implementation classes must be modified, including the Implementation classes made by many customers. Therefore, we had to use this compromise.
As a result, we are faced with a problem similar to multi-inheritance. If class Sub inherits two interfaces, Base1 and Base2, and these two interfaces have exactly the same two default methods, a conflict will occur. In this case, the Sub class must explicitly specify the implementation of which interface you want to use (or provide your own implementation) through overload ):
Public class Sub implements Base1, Base2 {
Public void hello (){
Base1.super. hello (); // use Base1
}
}
In addition to the default method, Java 8 interfaces can also be implemented using static methods:
Public interface MyInterf {
String m1 ();
Default String m2 (){
Return "Hello default method! ";
}
Static String m3 (){
Return "Hello static method in Interface! ";
}
}
4.4 Generator function)
Sometimes a stream data source is not necessarily an existing collection object, or it may be a "generator function ". A generator function generates a series of elements to provide a stream. Stream. generate (Supplier S) is a generator function. The Supplier parameter is a function interface with a unique abstract method. Get ().
The following example generates and prints five random numbers:
Stream. generate (Math: random). limit (5). forEach (System. out: println );
Note that this limit (5) statement will be executed forever without this call. That is to say, this generator is infinite. Such a call is called an end operation or a short circuit (short-circuiting) operation.
References:
Http://openjdk.java.net/projects/lambda/
Http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html