Introduction to Lambda
Lambda expressions are an important new feature in Java SE 8. lambda expressions allow you to replace functional interfaces by expressions. The lambda expression is just like the method, which provides a normal parameter list and a body (body, which can be an expression or a code block) that uses these parameters.
Lambda expressions also enhance the collection library. Java SE 8 adds 2 packages that operate batch operations on collection data: java.util.function package and java.util.stream package. A stream is like an iterator, but with many additional features attached. In general, lambda expressions and streams are the biggest changes since the Java language adds generics and annotations.
Lambda expressions are essentially anonymous methods, and their underlying layer is implemented through invokedynamic directives to generate anonymous classes. It provides a simpler syntax and writing method, allowing you to replace functional interfaces with expressions. In the eyes of some people, Lambda can make your code more concise and not use it at all - this view is certainly OK, but the important thing is that lambda brings closures to Java. Thanks to Lamdba's support for collections, Lambda has greatly improved performance when traversing collections under multi-core processor conditions. In addition, we can process collections in the form of data streams - which is very attractive.
Lambda syntax
Lambda's syntax is extremely simple, similar to the following structure:
(parameters) -> expression
or
(parameters) -> { statements; }Lambda expressions are composed of three parts:
1. Paramates: A list of formal parameters in similar methods, the parameters here are parameters in the functional interface. The parameter types here can be explicitly declared or not declared but implicitly inferred by the JVM. In addition, when there is only one inference type, parentheses can be omitted.
2. ->: It can be understood as "being used"
3. Method body: It can be an expression or a code block, it is the implementation of the method in the functional interface. A code block can return a value or reversal nothing. The code block here is equivalent to the method body of the method. If it is an expression, you can also return a value or return nothing.
Let's use the following examples to illustrate:
//Example 1: No need to accept parameters, directly return 10()->10//Example 2: Accept two parameters of int type and return the sum of these two parameters (int x,int y)->x+y;//Example 2: Accept two parameters of x and y, the type of this parameter is inferred by the JVM based on the context, and returns the sum of the two parameters (x,y)->x+y;//Example 3: Accept a string and print the string to control, without reverse the result (String name)->System.out.println(name);//Example 4: Accept an inferred type parameter name and print the string to console name->System.out.println(name);//Example 5: Accept two String type parameters and output them separately, without reverse the result (String name,String name,String sex)->{System.out.println(name);System.out.println(sex)}//Example 6: Accept a parameter x and return twice the parameter x->2*xWhere to use Lambda
In [Functional Interface][1] we know that the target type of Lambda expression is a functional interface - each Lambda can match a given type through a specific functional interface. Therefore, a Lambda expression can be applied anywhere that matches its target type. The lambda expression must have the same parameter type as the abstract function description of the functional interface, its return type must also be compatible with the return type of the abstract function, and the exceptions it can throw are limited to the function description range.
Next, let's look at a custom functional interface example:
@FunctionalInterface interface Converter<F, T>{ T convert(F from);}First, use the interface in the traditional way:
Converter<String ,Integer> converter=new Converter<String, Integer>() { @Override public Integer convert(String from) { return Integer.valueOf(from); } }; Integer result = converter.convert("200"); System.out.println(result);Obviously there is no problem with this, so the next thing is the moment when Lambda comes on the field, using Lambda to implement the Converter interface:
Converter<String,Integer> converter=(param) -> Integer.valueOf(param); Integer result = converter.convert("101"); System.out.println(result);Through the above example, I think you have a simple understanding of the use of Lambda. Below, we are using a commonly used Runnable to demonstrate:
In the past we might have written this code:
new Thread(new Runnable() { @Override public void run() { System.out.println("hello lambda"); } }).start();In some cases, a large number of anonymous classes can make the code appear cluttered. Now you can use Lambda to make it simple:
new Thread(() -> System.out.println("hello lambda")).start();Method reference
Method reference is a simplified way to write Lambda expressions. The method referenced is actually an implementation of the method body of the Lambda expression, and its syntax structure is:
ObjectRef::methodName
The left side can be the class name or instance name, the middle is the method reference symbol "::", and the right side is the corresponding method name.
Method references are divided into three categories:
1. Static method reference
In some cases, we might write code like this:
public class ReferenceTest { public static void main(String[] args) { Converter<String ,Integer> converter=new Converter<String, Integer>() { @Override public Integer convert(String from) { return ReferenceTest.String2Int(from); } }; converter.convert("120"); } @FunctionalInterface interface Converter<F,T>{ T convert(F from); } static int String2Int(String from) { return Integer.valueOf(from); }}At this time, if you use static references, the code will be more concise:
Converter<String, Integer> converter = ReferenceTest::String2Int; converter.convert("120");2. Instance method reference
We might also write code like this:
public class ReferenceTest { public static void main(String[] args) { Converter<String, Integer> converter = new Converter<String, Integer>() { @Override public Integer convert(String from) { return new Helper().String2Int(from); } }; converter.convert("120"); } @FunctionalInterface interface Converter<F, T> { T convert(F from); } static class Helper { public int String2Int(String from) { return Integer.valueOf(from); } }}Also, using example methods to reference will appear more concise:
Helper helper = new Helper(); Converter<String, Integer> converter = helper::String2Int; converter.convert("120");3. Constructor method reference
Now let's demonstrate references to constructors. First we define a parent class Animal:
class Animal{ private String name; private int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void behavior(){ } } Next, we are defining two subclasses of Animal: Dog、Bird
public class Bird extends Animal { public Bird(String name, int age) { super(name, age); } @Override public void behavior() { System.out.println("fly"); }}class Dog extends Animal { public Dog(String name, int age) { super(name, age); } @Override public void behavior() { System.out.println("run"); }}Then we define the factory interface:
interface Factory<T extends Animal> { T create(String name, int age); }Next, we will use the traditional method to create objects of Dog and Bird classes:
Factory factory=new Factory() { @Override public Animal create(String name, int age) { return new Dog(name,age); } }; factory.create("alias", 3); factory=new Factory() { @Override public Animal create(String name, int age) { return new Bird(name,age); } }; factory.create("smook", 2);I wrote more than ten codes just to create two objects. Now let's try using the constructor reference:
Factory<Animal> dogFactory =Dog::new; Animal dog = dogFactory.create("alias", 4); Factory<Bird> birdFactory = Bird::new; Bird bird = birdFactory.create("smook", 3); This way the code appears clean and neat. When using Dog::new to penetrate objects, select the corresponding creation function by signing the Factory.create function.
Lambda's domain and access restrictions
The domain is the scope, and the parameters in the parameter list in the Lambda expression are valid within the scope of the Lambda expression (domain). In the Lambda expression, external variables can be accessed: local variables, class variables and static variables, but the degree of operation limitations is different.
Access local variables
Local variables outside the Lambda expression will be implicitly compiled by the JVM to final type, so they can only be accessed but not modified.
public class ReferenceTest { public static void main(String[] args) { int n = 3; Calculate calculate = param -> { //n=10; Compile error return n + param; }; calculate.calculate(10); } @FunctionalInterface interface Calculate { int calculate(int value); }}Access static and member variables
Inside Lambda expressions, static and member variables are readable and writable.
public class ReferenceTest { public int count = 1; public static int num = 2; public void test() { Calculate calculate = param -> { num = 10;//Modify static variable count = 3;//Modify member variable return n + param; }; calculate.calculate(10); } public static void main(String[] args) { } @FunctionalInterface interface Calculate { int calculate(int value); }}Lambda cannot access the default method of function interface
Java8 enhances interfaces, including default methods that can add default keyword definitions to interfaces. We need to note here that access to default methods does not support internally.
Lambda Practice
In the [Functional Interface][2] section, we mentioned that many functional interfaces are built into the java.util.function package, and now we will explain the commonly used functional interfaces.
Predicate interface
Enter a parameter and return a Boolean value, which contains many default methods for logical judgment:
@Test public void predictTest() { Predicate<String> predict = (s) -> s.length() > 0; boolean test = predict.test("test"); System.out.println("String length is greater than 0:" + test); test = predict.test(""); System.out.println("String length is greater than 0:" + test); Predicate<Object> pre = Objects::nonNull; Object ob = null; test = pre.test(ob); System.out.println("Object is not empty:" + test); ob = new Object(); test = pre.test(ob); System.out.println("Object is not empty:" + test); }Function interface
Receive a parameter and return a single result. The default method ( andThen ) can string multiple functions together to form a composite Funtion (with input, output) result.
@Test public void functionTest() { Function<String, Integer> toInteger = Integer::valueOf; //The execution result of toInteger is used as input to the second backToString Function<String, String> backToString = toInteger.andThen(String::valueOf); String result = backToString.apply("1234"); System.out.println(result); Function<Integer, Integer> add = (i) -> { System.out.println("frist input:" + i); return i * 2; }; Function<Integer, Integer> zero = add.andThen((i) -> { System.out.println("second input:" + i); return i * 0; }); Integer res = zero.apply(8); System.out.println(res); }Supplier interface
Returns a result of a given type. Unlike Function , Supplier does not need to accept parameters (supplier, with output but no input)
@Test public void supplierTest() { Supplier<String> supplier = () -> "special type value"; String s = supplier.get(); System.out.println(s); }Consumer interface
Represents the operations that need to be performed on a single input parameter. Unlike Function , Consumer does not return value (consumer, input, no output)
@Test public void consumerTest() { Consumer<Integer> add5 = (p) -> { System.out.println("old value:" + p); p = p + 5; System.out.println("new value:" + p); }; add5.accept(10); } The usage of the above four interfaces represents the four types in the java.util.function package. After understanding these four functional interfaces, other interfaces will be easy to understand. Now let’s make a simple summary:
Predicate is used for logical judgment, Function is used in places where there are inputs and outputs, Supplier is used in places where there is no input and outputs, and Consumer is used in places where there is input and no outputs. You can know the usage scenarios based on the meaning of its name.
Stream
Lambda brings closures for Java 8, which is particularly important in collection operations: Java 8 supports functional operations on the stream of collection objects. In addition, the stream API is also integrated into the collection API, allowing batch operations on collection objects.
Let’s get to know Stream.
Stream represents a data stream. It has no data structure and does not store elements themselves. Its operations will not change the source Stream, but generate a new Stream. As an interface for operating data, it provides filtering, sorting, mapping, and regulation. These methods are divided into two categories according to the return type: any method that returns the Stream type is called an intermediate method (intermediate operation), and the rest are completion methods (complete operation). The completion method returns a value of some type, while the intermediate method returns a new Stream. The call of intermediate methods is usually chained, and the process will form a pipeline. When the final method is called, it will cause the value to be consumed immediately from the pipeline. Here we must remember: Stream operations run as "delayed" as possible, which is what we often call "lazy operations", which will help reduce resource usage and improve performance. For all intermediate operations (except sorted) they are run in delay mode.
Stream not only provides powerful data operation capabilities, but more importantly, Stream supports both serial and parallelism. Parallelism allows Stream to have better performance on multi-core processors.
The use process of Stream has a fixed pattern:
1. Create a Stream
2. Through intermediate operations, "change" the original Stream and generate a new Stream
3. Use the completion operation to generate the final result
That is
Create -> Change -> Complete
Creation of Stream
For a collection, it can be created by calling the collection's stream() or parallelStream() . In addition, these two methods are also implemented in the Collection interface. For arrays, they can be created by Stream's static method of(T … values) . In addition, Arrays also provides support for streams.
In addition to creating Streams based on collections or arrays above, you can also create an empty Stream through Steam.empty() , or use Stream's generate() to create infinite Streams.
Let’s take serial Stream as an example to illustrate several commonly used intermediate and completion methods of Stream. First create a List collection:
List<String> lists=new ArrayList<String >(); lists.add("a1"); lists.add("a2"); lists.add("b1"); lists.add("b2"); lists.add("b3"); lists.add("o1");Intermediate method
Filter
Combined with the Predicate interface, Filter filters all elements in the streaming object. This operation is an intermediate operation, which means you can perform other operations based on the result returned by the operation.
public static void streamFilterTest() { lists.stream().filter((s -> s.startsWith("a"))).forEach(System.out::println); //Equivalent to the above operation Predicate<String> predicate = (s) -> s.startsWith("a"); lists.stream().filter(predicate).forEach(System.out::println); //Continuous filtering Predicate<String> predicate1 = (s -> s.endsWith("1")); lists.stream().filter(predicate).filter(predicate1).forEach(System.out::println); }Sort (Sorted)
Combined with the Comparator interface, this operation returns a view of the sorted stream, and the order of the original stream will not change. The collation rules are specified through Comparator, and the default is to sort them in natural order.
public static void streamSortedTest() { System.out.println("Default Comparator"); lists.stream().sorted().filter((s -> s.startsWith("a"))).forEach(System.out::println); System.out.println("Custom Comparator"); lists.stream().sorted((p1, p2) -> p2.compareTo(p1)).filter((s -> s.startsWith("a"))).forEach(System.out::println); }Map (Map)
Combined with the Function interface, this operation can map each element in the stream object into another element, realizing element type conversion.
public static void streamMapTest() { lists.stream().map(String::toUpperCase).sorted((a, b) -> b.compareTo(a)).forEach(System.out::println); System.out.println("Custom Mapping Rules"); Function<String, String> function = (p) -> { return p + ".txt"; }; lists.stream().map(String::toUpperCase).map(function).sorted((a, b) -> b.compareTo(a)).forEach(System.out::println); }The above briefly introduces three commonly used operations, which greatly simplify the processing of the collection. Next, we introduce several ways to complete:
Finishing method
After the "transform" process, the result needs to be obtained, that is, the operation is completed. Let's look at the related operations below:
Match
Used to determine whether a predicate matches the stream object, and finally returns a Boolean type result, for example:
public static void streamMatchTest() { //Return true as long as one element in the stream object matches boolean anyStartWithA = lists.stream().anyMatch((s -> s.startsWith("a"))); System.out.println(anyStartWithA); //Return true when each element in the stream object matches boolean allStartWithA = lists.stream().allMatch((s -> s.startsWith("a"))); System.out.println(allStartWithA); }Collect
After the transformation, we collect the elements of the transformed Stream, such as saving these elements into a collection. At this time, we can use the collect method provided by Stream, for example:
public static void streamCollectTest() { List<String> list = lists.stream().filter((p) -> p.startsWith("a")).sorted().collect(Collectors.toList()); System.out.println(list); }Count
SQL-like count is used to count the total number of elements in the stream, for example:
public static void streamCountTest() { long count = lists.stream().filter((s -> s.startsWith("a"))).count(); System.out.println(count); }Reduce
reduce method allows us to calculate elements in our own way or associate elements in a Stream with some pattern, for example:
public static void streamReduceTest() { Optional<String> optional = lists.stream().sorted().reduce((s1, s2) -> { System.out.println(s1 + "|" + s2); return s1 + "|" + s2; }); }The execution results are as follows:
a1|a2a1|a2|b1a1|a2|b1|b2a1|a2|b1|b2|b3a1|a2|b1|b2|b3|o1
Parallel Stream vs Serial Stream
So far, we have introduced the commonly used intermediate and completed operations. Of course all examples are based on serial Stream. Next, we will introduce the key drama - parallel Stream (parallel Stream). Parallel Stream is implemented based on the Fork-join parallel decomposition framework, and divides the big data set into multiple small data and hands it over to different threads for processing. In this way, performance will be greatly improved under the situation of multi-core processing. This is consistent with the design concept of MapReduce: large tasks become smaller, and small tasks are reassigned to different machines for execution. But the small task here is handed over to different processors.
Create a parallel Stream via parallelStream() . To verify whether parallel Streams can really improve performance, we execute the following test code:
First create a larger collection:
List<String> bigLists = new ArrayList<>(); for (int i = 0; i < 10000000; i++) { UUID uuid = UUID.randomUUID(); bigLists.add(uuid.toString()); }Test the time to sort under serial streams:
private static void notParallelStreamSortedTest(List<String> bigLists) { long startTime = System.nanoTime(); long count = bigLists.stream().sorted().count(); long endTime = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime); System.out.println(System.out.printf("Serial Sort: %d ms", millis)); }Test the time to sort in parallel streams:
private static void parallelStreamSortedTest(List<String> bigLists) { long startTime = System.nanoTime(); long count = bigLists.parallelStream().sorted().count(); long endTime = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime); System.out.println(System.out.printf("ParallelSorting: %d ms", millis)); }The results are as follows:
Serial sort: 13336 ms
Parallel sort: 6755 ms
After seeing this, we did find that the performance has improved by about 50%. Do you also think that you will be able to use parallel Stream in the future? In fact, it is not the case. If you are still a single-core processor now and the data volume is not large, serial streaming is still such a good choice. You will also find that in some cases, the performance of serial streams is better. As for the specific use, you need to test it first and then decide according to the actual scenario.
Lazy operation
Above we talked about Stream running as late as possible, and here we explain it by creating an infinite Stream:
First, use the Stream generate method to create a natural number sequence, and then transform the Stream through map :
//Incremental sequence class NatureSeq implements Supplier<Long> { long value = 0; @Override public Long get() { value++; return value; } } public void streamCreateTest() { Stream<Long> stream = Stream.generate(new NatureSeq()); System.out.println("Number of elements: "+stream.map((param) -> { return param; }).limit(1000).count()); }The execution result is:
Number of elements: 1000
We found that at the beginning, any intermediate operations (such as filter,map , etc., but sorted cannot be done) are OK. That is, the process of performing intermediate operations on the Stream and surviving a new Stream does not take effect immediately (or the map operation in this example will run forever and be blocked), and the stream starts to calculate when the completion method is encountered. Through limit() method, convert this infinite Stream into a finite Stream.
Summarize
The above is all the contents of the quick introduction to Java Lambda. After reading this article, do you have a deeper understanding of Java Lambda? I hope this article will be helpful to everyone to learn Java Lambda.