Deep analysis: the new features of java8, lambda and stream stream, have you learned it?

Time:2020-12-10

1. Lambda expression

1.1 what is lambda

Take Java as an example, you can assign a value to a Java variable, such as int a = 1. For a method, a block of code is also assigned to a variable. For this code, or the function assigned to a variable, it is a lambda expression

//Assign values to variables
int a = 1;

//Assign code blocks to variables
var = public void fun(int x){
    x+1;
}

//It can be simplified
var = (x)->x+1;

1.2 why does Java introduce lambda

Lambda is for functional programming
What is functional programming?
Functional programming is a programming paradigm, that is, the methodology of how to write programs. The main idea is to write the operation process into a series of nested function calls. FP emphasizes “everything is lambda”, and emphasizes the importance of invariance in logic processing

OOP emphasizes “everything is object” and message passing between objects. Message passing is used to change the internal state of each object. However, in many cases, the code can’t use the object. For example, to process a group of data, first query, then aggregate, then sort, then join, sort, aggregate, and then map to get the final result. This process, using FP function is very natural

result = func1(func2(func3...funcN(x))))

In order to increase the use of functional programming in the original OOP idea, Java adds the new features of lambda function in java8

In addition, the introduction of lambda expression also makes the code more concise, which can avoid generating too many useless implementation classes that pollute the environment

1.3 how to use lambda expression

The introduction of lambda expression can avoid generating too many implementation classes that pollute the environment;
Lambda expressions can be assigned to a variable. What is the type of the variable?
In Java, all types of lambda are an interface, and the lambda expression itself needs to be the implementation of this interface. This interface needs to have three characteristics. The interface with these characteristics is called functional interface

Functional interfaces have only one abstract method
The default method is the default implementation and does not include abstract methods
If the interface declares an override java.lang.Object , then it will not be counted in the number of abstract methods of the interface, because any implementation of the interface will have java.lang.Object Or other places
How to use lambda expression
For example, the comparator interface is a functional interface, so it can use lambda expression. Before using comparator to sort a list, it is as follows

List<Integer> list = new ArrayList<>();
Collections.sort(list, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1-o2;
    }
});

As you can see, the real useful one above is return O1 – O2. The code above is written with lambda expression as follows

Collections.sort(list, ((o1, o2) -> o1-o2));

Basic syntax of lambda expression: lambda operator > split lambda expression into two parts
Left: parameter list of lambda expression;
Right: the function to be executed in lambda expression, namely lambda body;

Syntax format 1: no parameter, no return value
() -> System.out.println("Hello Lambda!");

Syntax format 2: there is a parameter and no return value
(x) -> System.out.println(x)

Syntax format 3: if there is only one parameter, the parenthesis can be omitted
x -> System.out.println(x)

Syntax format 4: there are more than two parameters, return value, and there are multiple statements in lambda body
Comparator<Integer> com = (x, y) -> {
System.out.println ("interface function");
return Integer.compare(x, y);
};

If there is only one sentence in lambda body, return and curly brackets can be omitted
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

1.4 lambda expression method reference, constructor reference and array reference

Method reference
If the function in lambda body has already been implemented by a method, method reference can be used

Object reference: instance method name
Class name: static method name
Class name: instance method name

① The parameter list and return value type of the method referenced by the method should be consistent with the parameter list and return value type of the abstract method in the functional interface!
② If the first parameter of lambda’s parameter list is the caller of the instance method, and the second parameter (or no parameter) is the parameter of the instance method, the format is classname:: methodname

//Object reference: instance method name
@Test
public void test1(){
        //This is what we wrote before
    Employee EMP = new employee (101, "Zhang San", 18, 9999);
    Supplier<String> sup = () -> emp.getName();
    System.out.println(sup.get());  

    System.out.println("----------------------------------");   
        //Now that's how we write it
    Supplier<String> sup2 = emp::getName;
    System.out.println(sup2.get());
}

//Class name: static method name
@Test
public void test2(){
    Comparator<Integer> com = (x, y) -> Integer.compare(x, y);  
    System.out.println("-------------------------------------");     
    Comparator<Integer> com2 = Integer::compare;
}

//Class name: instance method name
@Test
public void test3(){
    BiPredicate<String, String> bp = (x, y) -> x.equals(y);
    System.out.println(bp.test("abcde", "abcde"));
        
    System.out.println("-----------------------------------------");
        
    BiPredicate<String, String> bp2 = String::equals;
    System.out.println(bp2.test("abc", "abc"));

}

Constructor reference

For the person class, there are two constructors

class Person {
    String firstName;
    String lastName;

    Person() {}

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

There is now a factory interface to generate the person class

//Person factory
interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}

We can use the:: keyword to refer to the constructor of the person class instead of manually implementing the factory interface

//Direct reference to person constructor
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

The code of person:: new can directly refer to the constructor of the person class. Then the java compiler can select the right constructor to implement according to the context PersonFactory.create method

2.1 what is stream

Java 8 introduces a new stream API. The stream here is different from the I / O stream. The stream in Java 8 is an enhancement of the collection object function. It focuses on various convenient and efficient aggregation operations on collection objects, or mass data operation. Stream API relies on lambda, which is also a new emerging technology Expression, greatly improve programming efficiency and program readability

A stream is just like an iterator. It is unidirectional and non reciprocating. The data can only be traversed once, and it is exhausted after traversing once. It is just like flowing water in front of you and never returning

List<String> myList =
    Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
    . stream() // create stream
    . filter (s - > s.startswith ("C") // performs filtering to filter out strings prefixed with C
    . map (string:: touppercase) // convert to uppercase
    . sorted() // sort
    .forEach( System.out :: println); // for loop printing

① : an intermediate operation will return a stream again, so we can link multiple intermediate operations. Note that semicolons are not required here. In the above figure, filter filtering, map object conversion and sorted sorting are intermediate operations.
② : a terminal operation is an end action of a stream operation, which generally returns void or a non stream result. The foreach loop in the figure above is a terminate operation

The above is the simple and practical application of stream. It can be seen that it is also functional programming, which expresses more business logic

2.2 common API

Create stream

1. Arrays.stream()

When facing an array in daily programming, you can use the Arrays.stream () method to use stream

Integer[] array = new Integer[]{3,4,8,16,19,27,23,99,76,232,33,96};
long count = Arrays.stream(array).filter(i->i>20).count();

2. Stream.of()

When facing an array, you can use the Arrays.stream In addition to the () method, you can also use stream to convert the required array into a stream. This method not only supports passing in arrays and converting arrays into streams, but also supports passing in multiple parameters to convert parameters into streams

Integer[] array = new Integer[]{3,4,8,16,19,27,23,99,76,232,33,96};
long count = Stream.of(array).filter(i->i>20).count();
long sum = Stream.of(12,77,59,3,654).filter(i->i>20).mapToInt(Integer::intValue).sum();
System.out.println("count:"+count+",sum:"+sum);

3. Collection.stream()

This is the most common stream. Because collection is the parent interface of the collection interface in Java, all collections in Java inherit or implement this interface. This method can be used to create a Java stream collection

List<Integer> numbers = new ArrayList<>();
numbers.add(3);
numbers.add(4);
numbers.add(8);
numbers.add(16);   
numbers.stream().forEach(number->{
    System.out.println(number);
});

4.filter

This is a filter transformation of a stream. This method generates a new stream containing all elements that meet a certain condition. The filter takes a function as a parameter, which is represented by a lambda expression

List<Integer> integerList = Lists.newArrayList();
integerList.add(15);
integerList.add(32);
integerList.add(5);
integerList.add(232);
integerList.add(56);
List<Integer> after = integerList.stream()
                    .filter(i->i>50)
                    .collect(Collectors.toList());
System.out.println(after);//232,56

5.map

The map method refers to some form of transformation of values in a stream. You need to pass it a converted function as an argument

List<Integer> integerList = Lists.newArrayList();
integerList.add(15);
integerList.add(32);
integerList.add(5);
integerList.add(232);
integerList.add(56);
//Converts an integer type to a string type
List<String> afterString = integerList.stream()
                .map(i->String.valueOf(i)).collect(Collectors.toList());

6.flatMap

Connect multiple streams into a stream. At this time, instead of replacing the value of the stream with a new value, which is different from map, it is to regenerate a stream object instead

List<String> words = new ArrayList<String>();
words.add("your");
words.add("name");


public static Stream<Character> characterStream(String s){  
    List<Character> result = new ArrayList<>();  
    for (char c : s.toCharArray()) 
        result.add(c);
    return result.stream();  
}
  
Stream<Stream<Character>> result = words.map(w -> characterStream(w));  
//[['y', 'o', 'u', 'r'], ['n', 'a', 'm', 'e']]
Stream<Character> letters = words.flatMap(w -> characterStream(w));
//['y', 'o', 'u', 'r', 'n', 'a', 'm', 'e'] 

7. Limit method and skip method

The limit (n) method returns a new stream with n elements (if the total length is less than N, the original stream is returned)
The skip (n) method is the opposite. It discards the first n elements
The daily paging function can be realized by using the limit and skip methods together

List<Integer> pageList = myList.stream()
                  .skip(pageNumber*pageSize)
                  .limit(pageSize).collect(Collectors.toList());

8. Distinct method and sorted method

The distinct method returns a stream with the same order and no duplicate elements based on the elements in the original stream. This operation obviously needs to remember the previously read elements.

List<Integer> myTestList = Lists.newArrayList();
myTestList.add(10);
myTestList.add(39);
myTestList.add(10);
myTestList.add(78);
myTestList.add(10);
List<Integer> distinctList = myTestList.stream()
                        .distinct().collect(Collectors.toList());
System.out.println("distinctList:"+distinctList);
Operation results:
distinctList:[10, 39, 78]

The sorted method needs to traverse the entire flow and sort it before generating any elements. Because it is possible that the first element of the set after sorting will be in the last bit of the unsorted set.

List<Integer> myTestList = Lists.newArrayList();
myTestList.add(39);
myTestList.add(78);
myTestList.add(10);
myTestList.add(22);
myTestList.add(56);
List<Integer> sortList = myTestList.stream()
                .sorted(Integer::compareTo).collect(Collectors.toList());
System.out.println("sortList:"+sortList);
Operation results:
sortList:[10, 22, 39, 56, 78]

9.Collect

Collect generates lists, maps, and other common data structures in the stream

To collect a stream into a list, just write it like this.
List<Integer> thereList = hereList.stream().collect(Collectors.toList());

It can be used in this way
Set<Integer> thereSet = hereList.stream().collect(Collectors.toSet());

When a set is collected, it controls the type of the set.
TreeSet<Integer> treeSet = hereList.stream()
                    .collect(Collectors.toCollection(TreeSet::new));

10. Polymerization operation

Aggregation is the aggregation of streams into a value for use in a program. The aggregation methods include sum, count, Max, min

long sum = Stream.of(12,77,59,3,654).filter(i->i>20).mapToInt(Integer::intValue).sum();
The findfirst method returns the first value in a non empty collection, which is usually used in conjunction with the filter method
Integer first = hearList.stream().filter(i->i>100).findFirst().get();

Findany method can return as long as any matching element is found in the collection. This method is very effective in parallel execution of convection
Integer anyItem = hearList.parallelStream().filter(i->i>100).findAny().get();

11. Grouping

Grouping values with the same properties is a common feature

Group a collection of room objects by height.

List<Room> roomList = Lists.newArrayList(
new Room(11,23,56),
new Room(11,84,48),
new Room(22,46,112),
new Room(22,75,62),
new Room(22,56,75),
new Room(33,92,224));

Map<Integer,List<Room>> groupMap = roomList.stream().collect(Collectors.groupingBy(Room::getHigh));
System.out.println("groupMap:"+groupMap);

2.3 processing sequence of stream stream

The intermediate operation of stream stream is delayed, and only when there is terminal operation, the intermediate operation will be executed

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    });

When executing this code segment, nothing will be printed. If you add a foreach terminal operation to the above code, you will have the printed content

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    })
    .forEach(s -> System.out.println("forEach: " + s));
filter:  d2
forEach: d2
filter:  a2
forEach: a2
filter:  b1
forEach: b1
filter:  b3
forEach: b3
filter:  c
forEach: c

However, we can see that the output result does not print all the print statements of filter operation first; in fact, the output result moves vertically along with the chain. For example, when the stream starts to process the D2 element, it will actually execute the foreach operation after the filter operation, and then process the second element

The reason is performance. This design reduces the actual number of operands for each element, such as the following

Stream.of("d2", "a2", "b1", "b3", "c")
    .map(s -> {
        System.out.println("map: " + s);
        Return s.touppercase(); // convert to uppercase
    })
    .anyMatch(s -> {
        System.out.println("anyMatch: " + s);
        Return s.startswith ("a"); // filter out the elements prefixed with a
    });

// map:      d2
// anyMatch: D2
// map:      a2
// anyMatch: A2

The terminal operation anymatch() means that any element prefixed with a and returns true stops the loop. So it starts matching from D2, and then when it loops to A2, it returns true and stops the loop.

Because the chained calls to the data stream are executed vertically, map only needs to be executed twice. Map executes as few times as possible relative to horizontal execution, rather than map all elements once

stream --> filter --> map --> sorted --> collect

2.4 parallel flow

Unlike iterators, streams can operate in parallel, while iterators can only operate in a command and serial manner. As the name suggests, when traversing in serial mode, each item is read before the next item;
Stream has the ability of parallel processing, and the processing process will be divided and governed, that is, a large task is divided into multiple small tasks, which means that each task is an operation

//The parallel method can convert any serial stream into a parallel stream
Stream.of(roomList).parallel();
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream()
       .forEach(out::println);  
//The order of presentation is not necessarily 1, 2, 3, 4, 5, 6, 7, 8, 9, but may be in any order

last

You are welcome to read the message below