Java 8 features explain lambda expressions: lambda in streaming processing

To talk about Stream, you have to talk about its right arm Lambda and method reference. The Stream API you use is actually a functional programming style, in which "function" is a method reference, and "formula" is a Lambda expression.

Lambda expression

Lambda expression is a Anonymous function , Lambda expressions are based on λ calculus Named, which directly corresponds to the lambda abstraction, is an anonymous function, that is, a function without a function name. Lambda expressions can represent closures.

In Java, the format of Lambda expression is like this

// No parameter, no return value
() -> log.info("Lambda")

 // There are parameters and return values
(int a, int b) -> { a+b }
Copy code

It is equivalent to

log.info("Lambda");

private int plus(int a, int b){
   return a+b;
}
Copy code

The most common example is to create a new Thread. Sometimes, in order to save trouble, the following method is used to create and start a Thread. This is the writing method of anonymous internal class. New Thread requires an object instance of implements from Runnable type as a parameter. A better way is to create a new class, implements Runnable, Then new gives an instance of the new class and passes it to Thread as a parameter. The anonymous inner class does not need to find an object to receive, but directly as a parameter.

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Quickly create and start a thread");
    }
}).start();
Copy code

But does it feel messy and rustic to write like this? At this time, Lambda expression is another feeling.

new Thread(()->{
    System.out.println("Quickly create and start a thread");
}).start();
Copy code

How about this change? I feel fresh and refined, concise and elegant.

Lambda expressions simplify the form of anonymous inner classes and can achieve the same effect, but lambda is much more elegant. Although the ultimate goal is the same, the internal implementation principle is different.

The anonymous inner class will create a new anonymous inner class after compilation, and Lambda is implemented by calling the JVM invokedynamic instruction and will not generate a new class.

Method reference

The emergence of method references enables us to assign a method to a variable or pass it as a parameter to another method.:: Double colons are used as symbols for method references. For example, the following two lines of statements refer to the parseInt method of Integer class.

Function<String, Integer> s = Integer::parseInt;
Integer i = s.apply("10");
Copy code

Or the following two lines refer to the compare method of the Integer class.

Comparator<Integer> comparator = Integer::compare;
int result = comparator.compare(100,10);
Copy code

For another example, the following two lines of code also refer to the compare method of Integer class, but the return types are different, but both can execute normally and return correctly.

IntBinaryOperator intBinaryOperator = Integer::compare;
int result = intBinaryOperator.applyAsInt(10,100);
Copy code

I believe some students are afraid that this is the following state. Is it completely unreasonable? It's too casual. You can take the offer back to anyone.

Take it easy. Come on. Now let's solve our doubts and remove the mask.

Q: What methods can be referenced?

A: Well, any method you have access to can be referenced.

Q: What exactly is the return value type?

A: That's the point. There are functions, comparators and intbinaryoperators on it. It seems that there is no rule, but it's not.

The returned type is a functional interface specifically defined by Java 8, which is annotated with @ functional interface.

For example, the functional interface Function is defined as follows:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Copy code

Another key point is that the number of parameters, types and return value types of your referenced method should correspond to the method declaration in the functional interface one by one.

For example, integer The parseInt method is defined as follows:

public static int parseInt(String s) throws NumberFormatException {
    return parseInt(s,10);
}
Copy code

Firstly, the number of parameters of the parseInt method is 1, and the number of parameters of the apply method in the Function is also 1. The number of parameters corresponds to the number of parameters. Thirdly, the parameter type and return type of the apply method are generic types, so they must correspond to the parseInt method.

In this way, you can correctly receive the method reference of Integer::parseInt and call the apply method of functon. At this time, the corresponding integer is actually called ParseInt method.

Applying this set of standards to the Integer::compare method, it is not difficult to understand why it can be received by comparator < integer > and IntBinaryOperator, and calling their respective methods can correctly return results.

Integer. The compare method is defined as follows:

public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
Copy code

The return value type is int, two parameters, and the parameter type is int.

Then look at the functional interface definitions of Comparator and IntBinaryOperator and their corresponding methods:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}
Copy code

Right or wrong, can be matched correctly, so the two functional interfaces in the previous example can receive normally. In fact, there are more than these two methods. As long as such a method is declared in a functional interface: two parameters, the parameter type is int or generic, and the return value is int or generic, can be received perfectly.

JDK defines many functional interfaces, mainly in Java util. Under the function package, there is also Java util. Comparator is specifically used as a custom comparator. In addition, Runnable mentioned earlier is also a functional interface.

Do it yourself example

1. Define a functional interface and add a method

A functional interface named KiteFunction is defined, annotated with @ FunctionalInterface, and then a method run with two parameters is declared, both of which are generic types, and the return result is also generic.

Another important point is that only one method that can be implemented can be declared in a functional interface. You can't declare a run method and a start method. At that time, the compiler won't know which method to receive. The method decorated with the default keyword has no effect.

@FunctionalInterface
public interface KiteFunction<T, R, S> {

    /**
     * Define a two parameter method
     * @param t
     * @param s
     * @return
     */
    R run(T t,S s);
}
Copy code

2. Define a method corresponding to the run method in KiteFunction

The function test class defines the method DateFormat, a method that formats the LocalDateTime type into a string type.

public class FunctionTest {
    public static String DateFormat(LocalDateTime dateTime, String partten) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
        return dateTime.format(dateTimeFormatter);
    }
}
Copy code

3. Call by method reference

Normally, we use functiontest directly Dateformat () is OK.

And in a functional way, it's like this.

KiteFunction<LocalDateTime,String,String> functionDateFormat = FunctionTest::DateFormat;
String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");
Copy code

In fact, instead of specifically defining the DateFormat method outside, I can use the anonymous inner class as follows.

public static void main(String[] args) throws Exception {

    String dateString = new KiteFunction<LocalDateTime, String, String>() {
        @Override
        public String run(LocalDateTime localDateTime, String s) {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s);
            return localDateTime.format(dateTimeFormatter);
        }
    }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
    System.out.println(dateString);
}
Copy code

As mentioned in the first Runnable example above, such an anonymous inner class can be simplified in the form of Lambda expression. The simplified code is as follows:

public static void main(String[] args) throws Exception {

        KiteFunction<LocalDateTime, String, String> functionDateFormat = (LocalDateTime dateTime, String partten) -> {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
            return dateTime.format(dateTimeFormatter);
        };
        String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
        System.out.println(dateString);
}
Copy code

Use a Lambda expression such as (localdatetime, datetime, string partten) - > {} to directly return the method reference.

Stream API

In order to talk about the use of Stream API, it can be said that it takes a lot of trouble. Do you know what it is and why? The attitude and posture of pursuing technology should be correct.

Of course, stream is not just a Lambda expression. What's really powerful is its function. Stream is a sharp tool for collective data processing in Java 8. Many complex methods that need to write a lot of code, such as filtering, grouping and other operations, can often be done in one line of code by using stream. Of course, because stream is a chain operation, A single line of code may call several methods.

The Collection interface provides the stream() method, which allows us to easily use the Stream API for various operations in a Collection. It is worth noting that any operation we perform will not affect the source Collection. You can extract multiple streams from a Collection for operation at the same time.

Let's look at the definition of the Stream interface, which inherits from BaseStream. All interface declarations are parameters of the receiving method reference type. For example, the filter method receives a parameter of the Predicate type. It is a functional interface, which is commonly used for condition comparison, screening and filtering. JPA also uses this functional interface for query condition splicing.

public interface Stream<T> extends BaseStream<T, Stream<T>> {

  Stream<T> filter(Predicate<? super T> predicate);

  // Other interfaces
}  
Copy code

Let's take a look at the common API s of Stream.

of

It can receive a generic object or become a generic collection to construct a Stream object.

private static void createStream(){
    Stream<String> stringStream = Stream.of("a","b","c");
}
Copy code

empty

Create an empty Stream object.

concat

Connect two streams and return a new Stream object without changing any of the Steam objects.

private static void concatStream(){
    Stream<String> a = Stream.of("a","b","c");
    Stream<String> b = Stream.of("d","e");
    Stream<String> c = Stream.concat(a,b);
}
Copy code

max

It is generally used to find the maximum value in the number set, or compare the entity with the maximum value according to the number type attribute in the entity. It receives a comparator < T >, which is also mentioned above. It is a functional interface type, which is specially used to define the comparison between two objects. For example, the following method uses the method reference Integer::compareTo.

private static void max(){
    Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
    Integer max = integerStream.max(Integer::compareTo).get();
    System.out.println(max);
}
Copy code

Of course, we can also customize a Comparator by ourselves. By the way, we can review the method reference in the form of Lambda expression.

private static void max(){
    Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
    Comparator<Integer> comparator =  (x, y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y)) ? 0 : 1);
    Integer max = integerStream.max(comparator).get();
    System.out.println(max);
}
Copy code

min

It's the same as max, except for the minimum.

findFirst

Gets the first element in the Stream.

findAny

Get an element in the Stream. If it is serial, it will generally return the first element, but not necessarily in parallel.

count

Returns the number of elements.

Stream<String> a = Stream.of("a", "b", "c");
long x = a.count();
Copy code

peek

Establish a channel in which corresponding operations are performed on each element of the Stream, corresponding to the functional interface of consumer < T >, which is a consumer functional interface. As the name suggests, it is used to consume Stream elements. For example, the following method converts each element into a corresponding uppercase letter and outputs it.

private static void peek() {
    Stream<String> a = Stream.of("a", "b", "c");
    List<String> list = a.peek(e->System.out.println(e.toUpperCase())).collect(Collectors.toList());
}
Copy code

forEach

Similar to the peek method, it receives a consumer functional interface and can perform corresponding operations on each element. However, unlike peek, after forEach is executed, the Stream is really consumed, and then the Stream is gone. Subsequent operations on it are not allowed. After peek is completed, It is also an operable Stream object.

Just by this way, when we use the Stream API, it is a series of chain operations. This is because many methods, such as the filter method to be mentioned next, return values are of the Stream type, that is, Stream objects processed by the current method, so the Stream API can still be used.

private static void forEach() {
    Stream<String> a = Stream.of("a", "b", "c");
    a.forEach(e->System.out.println(e.toUpperCase()));
}
Copy code

forEachOrdered

The function is the same as forEach. The difference is that forEachOrdered is guaranteed in order, that is, the elements in the Stream are consumed in the order of insertion. Why? When parallelism is enabled, the effects of forEach and forEachOrdered are different.

Stream<String> a = Stream.of("a", "b", "c");
a.parallel().forEach(e->System.out.println(e.toUpperCase()));
Copy code

When using the above code, the output result may be B, A, C or A, C, B or A, B, C, while using the following code, it will be A, B, C every time

Stream<String> a = Stream.of("a", "b", "c");
a.parallel().forEachOrdered(e->System.out.println(e.toUpperCase()));
Copy code

limit

Get the first n pieces of data, similar to MySQL's limit, but can only receive one parameter, that is, the number of data pieces.

private static void limit() {
    Stream<String> a = Stream.of("a", "b", "c");
    a.limit(2).forEach(e->System.out.println(e));
}
Copy code

The printing results of the above code are a and b.

skip

Skip the first n pieces of data, such as the following code, and the return result is c.

private static void skip() {
    Stream<String> a = Stream.of("a", "b", "c");
    a.skip(2).forEach(e->System.out.println(e));
}
Copy code

distinct

Element de duplication. For example, the following method returns elements a, b, and c, leaving only one duplicate b.

private static void distinct() {
    Stream<String> a = Stream.of("a", "b", "c","b");
    a.distinct().forEach(e->System.out.println(e));
}
Copy code

sorted

There are two overloads, one with no parameters and the other with parameters of Comparator type.

Nonparametric types are sorted in natural order, which is only suitable for simple elements, such as numbers, letters, etc.

private static void sorted() {
    Stream<String> a = Stream.of("a", "c", "b");
    a.sorted().forEach(e->System.out.println(e));
}
Copy code

If there are parameters, you need to customize the sorting rules. For example, the following method sorts according to the size of the second letter. The final output results are a1, b3 and c6.

private static void sortedWithComparator() {
    Stream<String> a = Stream.of("a1", "c6", "b3");
    a.sorted((x,y)->Integer.parseInt(x.substring(1))>Integer.parseInt(y.substring(1))?1:-1).forEach(e->System.out.println(e));
}
Copy code

In order to better illustrate the following API s, I simulated several similar data often used in projects, including 10 user information.

private static List<User> getUserData() {
    Random random = new Random();
    List<User> users = new ArrayList<>();
    for (int i = 1; i <= 10; i++) {
        User user = new User();
        user.setUserId(i);
        user.setUserName(String.format("Ancient kites %s number", i));
        user.setAge(random.nextInt(100));
        user.setGender(i % 2);
        user.setPhone("18812021111");
        user.setAddress("nothing");
        users.add(user);
    }
    return users;
}
Copy code

filter

It is used to filter the qualified data. For example, the following method is used to filter out records with gender 0 and age greater than 50.

private static void filter(){
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    stream.filter(user -> user.getGender().equals(0) && user.getAge()>50).forEach(e->System.out.println(e));

    /**
     *Equivalent to the following anonymous inner class
     */
//    stream.filter(new Predicate<User>() {
//        @Override
//        public boolean test(User user) {
//            return user.getGender().equals(0) && user.getAge()>50;
//        }
//    }).forEach(e->System.out.println(e));
}
Copy code

map

The interface method of the map method is declared as follows. It accepts a Function function interface and translates it into a mapping. The new type is mapped through the original data element.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Copy code

The declaration of Function is as follows: observe the apply method, accept a T-type parameter and return an R-type parameter. It is appropriate to convert one type to another, which is also the original intention of map. It is used to change the type of the current element, such as converting Integer to String type, DAO entity type to DTO instance type.

Of course, the types of T and R can also be the same. In this case, it is no different from the peek method.

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}
Copy code

For example, the following method should be a common requirement of the business system to convert the User into the data format output by the API.

private static void map(){
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList());
}

private static UserDto dao2Dto(User user){
    UserDto dto = new UserDto();
    BeanUtils.copyProperties(user, dto);
    //Other additional treatment
    return dto;
}
Copy code

mapToInt

The element is converted into int type and encapsulated on the basis of map method.

mapToLong

The element is converted to Long type and encapsulated on the basis of map method.

mapToDouble

Convert the element to Double type and encapsulate it based on the map method.

flatMap

This is used in some special scenarios. When your Stream has the following structures, you need to use the flatMap method to flatten the original two-dimensional structure.

  1. Stream<String[]>
  2. Stream<Set<String>>
  3. Stream<List<String>>

The above three types of structures can be converted into stream < string > by flatMap method, which is convenient for other operations in the future.

For example, the following method flattens list < list < user > > and then uses map or other methods to operate.

private static void flatMap(){
    List<User> users = getUserData();
    List<User> users1 = getUserData();
    List<List<User>> userList = new ArrayList<>();
    userList.add(users);
    userList.add(users1);
    Stream<List<User>> stream = userList.stream();
    List<UserDto> userDtos = stream.flatMap(subUserList->subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList());
}
Copy code

flatMapToInt

Refer to flatMap for usage. Flatten the element into int type and encapsulate it on the basis of flatMap method.

flatMapToLong

Refer to flatMap for usage. Flatten the element into Long type and encapsulate it on the basis of flatMap method.

flatMapToDouble

Refer to flatMap for usage. Flatten the element into Double type and encapsulate it on the basis of flatMap method.

collection

After a series of operations, most of the time, our final results are not to obtain Stream type data, but to change the results into common data structures such as List and Map, and collection is to achieve this purpose.

Take the map method as an example. After converting the object type, the final result set we need is a list < userdto > type. Use the collect method to convert the Stream to the type we need.

The following is the definition of the collect interface method:

<R, A> R collect(Collector<? super T, A, R> collector);
Copy code

The following example shows how to filter out values greater than 7 from a simple Integer Stream, and then convert it into a list < integer > collection, using collectors Tolist () is the collector.

private static void collect(){
    Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
    List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(Collectors.toList());
}
Copy code

Many students said they didn't quite understand the meaning of this Collector. Let's look at the following code. This is another overloaded method of collect. You can understand that its parameters are executed in order. This is a process from the creation of ArrayList to the call of addAll method.

private static void collect(){
    Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
    List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(ArrayList::new, ArrayList::add,
            ArrayList::addAll);
}
Copy code

We actually use this logic when customizing collectors, but we don't need to customize them at all. Collectors have provided us with many ready to use collectors. For example, we often use collectors toList(),Collectors.toSet(),Collectors.toMap(). Another example is collectors Groupingby() is used to group. For example, in the following example, group according to the userId field, return the Map with userId as the key and List as the value, or return the number of each key.

// Return userid: List < user >
Map<String,List<User>> map = user.stream().collect(Collectors.groupingBy(User::getUserId));

// Return userId: number of each group
Map<String,Long> map = user.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting()));
Copy code

toArray

collection is the return list, map, etc. toArray is the return array. There are two overloads, one is an empty parameter, and the return is Object [].

The other receives an intfunction < R > type parameter.

@FunctionalInterface
public interface IntFunction<R> {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    R apply(int value);
}
Copy code

For example, if it is used as follows, the parameter is User []: new, that is, new is a User array, and the length is the length of the last Stream.

private static void toArray() {
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    User[] userArray = stream.filter(user -> user.getGender().equals(0) && user.getAge() > 50).toArray(User[]::new);
}
Copy code

reduce

Its function is to use the last calculation result in each calculation, such as summation operation. The sum of the first two numbers plus the sum of the third number, plus the fourth number, is added to the last number position, and the final return result is the working process of reduce.

private static void reduce(){
    Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
    Integer sum = integerStream.reduce(0,(x,y)->x+y);
    System.out.println(sum);
}
Copy code

In addition, many methods of Collectors use reduce, such as groupingBy, minBy, maxBy, and so on.

Parallel Stream

In essence, Stream is used for data processing. In order to speed up processing, Stream API provides a way to process Stream in parallel. Via users Parallelstream() or users Stream(). Parallel () is used to create parallel Stream objects. The supported API is almost the same as that of ordinary Stream.

The ForkJoinPool thread pool is used by default for parallel streams. Of course, customization is also supported, but it is generally unnecessary. The divide and conquer strategy of ForkJoin framework coincides with parallel Stream processing.

Although the word parallel sounds powerful, it is not correct to use parallel flow in all cases, and it is not necessary at all many times.

When should parallel stream operations be used or not?

  1. It sounds like nonsense to use parallel Stream only under multi-core CPU.
  2. In the case of small amount of data, ordinary serial Stream can be used, and the use of parallel Stream has little impact on performance.
  3. CPU intensive computing is suitable for using parallel Stream, while IO intensive using parallel Stream will be slower.
  4. Although the calculation is parallel and may be fast, collect merging is still used most of the time. If the merging cost is very high, parallel Stream is not suitable.
  5. Some operations, such as limit, findFirst, forEachOrdered and other operations that depend on element order, are not suitable for parallel streams.

Author: ancient kite

Link: https://juejin.cn/post/6844904184953700360

Source: rare earth Nuggets

The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.

Keywords: Java

Added by phbock on Mon, 13 Dec 2021 07:05:10 +0200