Section 2 Java8 Stream programming (Lambda expression) 2021-12-29

Posted by damien@damosworld.com on Wed, 05 Jan 2022 09:44:58 +0100

General directory of Java components

1 Stream overview

It belongs to a kind of structured programming. The main idea is to realize a series of operations on data through chain function calls.

In JDK8, Stream is an interface, which contains many methods that can operate on the data in the Stream. These methods fall into three categories:

  • Methods for creating Stream interface objects. These methods generally need to enter the original operand and then create a data pool
  • Returns the intermediate operation method of Stream type
  • Returns other types of methods that terminate the operation

1. Problems needing attention

  • All intermediate operations return streams, which can ensure "chain call".
  • These functions have a common task: to operate on every data in the Stream
  • The operation result of an intermediate function on the data pool will be used as the data input of the next intermediate operation function
  • All operations are irreversible. Once operated, the data in the data pool will be affected

2 inert evaluation

In Stream stream programming, there is an inert evaluation problem for the execution of intermediate methods: multiple intermediate operations can be connected to form a chain call. Unless the termination operation is executed at the end of the chain call, the intermediate operation will not perform any processing. That is, the execution of chain call will be triggered only when the operation execution is terminated. This method call method is called lazy evaluation.

2. Creation of stream

  • Create a stream using an array
  • Create a stream using a collection
  • Create digital stream
  • Custom flow
public class Test2 {

    // Create a stream using an array
    @Test
    public void test01() {
        int[] nums = {1,2,3};

        IntStream stream = IntStream.of(nums);
        IntStream stream1 = Arrays.stream(nums);
    }

    // Create a stream using a collection
    @Test
    public void test02() {
        // Create a stream using List
        List<String> names = new ArrayList<>();
        Stream<String> stream = names.stream();

        // Create a stream using Set
        Set<String> cities = new HashSet<>();
        Stream<String> stream1 = cities.stream();
    }

    // Create digital stream
    @Test
    public void test03() {
        // Create a Stream containing 1,2,3
        IntStream stream = IntStream.of(1, 2, 3);

        // Create a Stream containing a range of [1,5]
        IntStream range1 = IntStream.range(1, 5);
        // Create a Stream containing the range [1,5]
        IntStream range2 = IntStream.rangeClosed(1, 5);

        // new Random().ints() creates an infinite stream
        // limit(5) limits the number of elements in the flow to 5
        IntStream ints = new Random().ints().limit(5);
        // It is equivalent to the above writing
        IntStream ints1 = new Random().ints(5);
    }

    // Custom flow
    @Test
    public void test04() {
        // First, an infinite stream is generated, and then the number of elements is limited
        Stream<Double> stream = Stream.generate(() -> Math.random())
                                      .limit(5);
    }

}

3 parallel processing and serial processing

Our previous operations are processed by a thread in a serial manner one by one to the elements in the Stream. In order to improve the processing efficiency, Stream supports processing the elements in the Stream in a parallel manner.
Using Stream streaming programming is very simple for parallel operation. You don't need to create Thread multithreading yourself. You just need to call parallel() method to realize multithreading environment. Serial Stream by default

The default thread pool used by parallel() multithreading is forkjoinpool commonpool, and its default number of threads can be modified. You need to set a system property related to the thread pool.

  • Serial processing (default)
  • Parallel stream output (multiple modes are subject to the last set mode)
  • Thread number modification
  • Use a custom thread pool (if multiple processes in the current system use this default thread pool at the same time, task blocking will occur in most cases.) Note: wait(), notify() and notifyAll() methods must be called in the synchronization method or synchronization code block, and which object calls these methods will act as a synchronization lock.
public class Test4 {

    // Static method: print an element (black) and sleep once
    public static void print(int i) {
        String name = Thread.currentThread().getName();
        System.out.println(i + " -- " + name);
        try {
            TimeUnit.MILLISECONDS.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // Static method: print an element (red) and sleep once
    public static void printRed(int i) {
        String name = Thread.currentThread().getName();
        System.err.println(i + " -- " + name);
        try {
            TimeUnit.MILLISECONDS.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // Serial processing
    @Test
    public void test01() {
        IntStream.range(1, 100)
                // . sequential() / / default
                .peek(Test4::print) // Use the method to reference Test4::print
                .count();
    }

    // parallel processing 
    @Test
    public void test02() {
        IntStream.range(1, 100)
                .parallel()
                .peek(Test4::print)  
                .count();
    }

    // Serial parallel hybrid processing: parallel first and then serial, and the final execution effect is the latter: serial processing
    @Test
    public void test03() {
        IntStream.range(1, 100)
                .parallel()            // parallel processing 
                .peek(Test4::print)
                .sequential()          // Serial processing
                .peek(Test4::printRed)
                .count();
    }

    // Serial parallel hybrid processing: serial first and then parallel
    // The final execution effect is the latter: parallel processing
    @Test
    public void test04() {
        IntStream.range(1, 100)
                .sequential()          // Serial processing
                .peek(Test4::printRed)
                .parallel()            // parallel processing 
                .peek(Test4::print)
                .count();
    }

    // Modify the number of threads in the default thread pool
    @Test
    public void test05() {
        // Specifies that the number of threads in the default thread pool is 16, including the specified 15 and main threads
        String key = "java.util.concurrent.ForkJoinPool.common.parallelism";
        System.setProperty(key, "15");

        IntStream.range(1, 100)
                .parallel()            // parallel processing 
                .peek(Test4::print)
                .count();
    }

    // Using a custom thread pool
    @Test
    public void test06() {
        // Create a thread pool containing 4 threads
        ForkJoinPool pool = new ForkJoinPool(4);
        pool.submit(() -> IntStream.range(1, 100)
                                    .parallel()            // parallel processing 
                                    .peek(Test4::print)
                                    .count()
                    );

        // wait(), notify(), notifyAll() methods must be called in the synchronization method or synchronization code block,
        // And which object calls these methods, and which corresponding object acts as a synchronization lock
        synchronized (pool) {
            try {
                // The main thread is blocked here
                pool.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4. Intermediate operation of stream

The method that returns the Stream interface type is called an intermediate operation. According to the standard of "whether you need to know the results of previous element operations when these operations operate on an element in the current data pool", intermediate operations can be divided into stateless operations and stateful operations.

Whether the element is related to the front and back elements is not a stateless operation, but only related to itself.

Generally, the parameters of Lambda expressions do not need to display the declaration type. So for a given Lambda expression, how does the program know which function interface and parameter type it corresponds to? The compiler infers the target type through the context of the Lambda expression, and deduces the appropriate function interface by checking whether the input parameter type and return type of the Lambda expression are consistent with the method signature of the corresponding target type.

Stream.of("foo", "bar").map(s -> s.length()).filter(l -> l == 3);

From the type context of Stream, it can be deduced that the input and output parameters are String types, and from the return value of Function, it can be deduced that the parameters are integer types. Therefore, it can be deduced that the corresponding Function interface type is Function;

1 stateless operation

1 map (Function<T, R> action)

Its Function is to map the elements in the flow to the last value. Because its parameter is Function, it has one input and output. Because there is output, the map() operation affects the elements in the stream.

    @Test
    public void test01() {
        String words = "Beijing welcome you I love China";
        // Take out the elements in the flow one by one, complete all processes, and then execute the next.
        Stream.of(words.split(" "))    // The elements in the current stream are individual words
              .peek(System.out::print)      // The peek function is the same as that of forEach, which does not change the elements in the flow. forEach is the termination operation, and peek is not.
              .map(word -> word.length())   // The elements in the current stream are the length of each word
              .forEach(System.out::println);
    }

2 mapToXxx()

Its function is to map the elements in the stream to the elements of the specified type. Different streams can map to different element types, that is, they have different mapToXxx() methods.

    @Test
    public void test02() {
        IntStream.range(1, 10)
                .mapToObj(i -> "No." + i)
                .forEach(System.out::println);
    }

    @Test
    public void test03() {
        String[] nums = {"111", "222", "333"};
        Arrays.stream(nums)   // "111", "222", "333"
                .mapToInt(Integer::valueOf)    // 111, 222, 333
                .forEach(System.out::println);
    }

    @Test
    public void test04() {
        String words = "Beijing welcome you I love China";
        Stream.of(words.split(" "))    // The elements in the current stream are individual words
                .flatMap(word -> word.chars().boxed())
                .forEach(ch -> System.out.print((char)(ch.intValue())));
    }

    @Test
    public void test05() {
        String words = "Beijing welcome you I love China";
        Stream.of(words.split(" "))    // The elements in the current stream are individual words
                // The elements in the resulting stream are the letters of each word
                .flatMap(word -> Stream.of(word.split("")))
                .forEach(System.out::print);
    }

3 flatMap(Function<T, Stream> action)

This is a stateless operation. Its function is to map the elements in the stream into multiple values (streams), i.e. flat map. Its applicable scenario is that each original element in the stream is a collection. This method is used to break up all the collection elements and then add them to the stream.

Because its parameter is Function and has input and output, the flatMap() operation will affect the elements in the Stream. Note that the output type of this Function is Stream.
The function of the following example is to output all the letters in the specified string.

    @Test
    public void test07() {
        String words = "Beijing welcome you I love China";
        Stream.of(words.split(" "))    // The elements in the current stream are individual words
                .flatMap(word -> Stream.of(word.split("")))
                .distinct()   // Remove duplicate letters
                .forEach(System.out::print); //BeijngwlcomyuIvCha
    }

4 filter(Predicate action)

Used to filter out elements in the stream that do not fit the specified conditions. Its parameter is Predicate assertion, which is used to set filter conditions.

    @Test
    public void test06() {
        String words = "Beijing welcome you I love China";
        Stream.of(words.split(" "))    // The elements in the current stream are individual words
                // When the filter condition is true, the current element will remain in the flow, otherwise it will be deleted from the flow
               .filter(word -> word.length() > 4)
               .forEach(System.out::println);
    }

5 peek(Consumer<? super T> action)

The peek function is the same as forEach. The consumer has no return value and does not change the elements in the flow. forEach is the termination operation, and peek is not.

2 stateful operation

1 distinct()

Filter out duplicate elements in the stream without parameters.

2 sorted() or sorted(Comparator c)

Sort the elements in the stream. sorted() without parameters is sorted by dictionary by default, that is, sorted by ASCII. You can specify the collation using the parameterized method.

3 skip(long)

Removes the specified number of elements from the stream. Count from the front

5. Termination of stream

The end of the termination operation means the end of the Stream operation. According to whether the operation needs to traverse all elements in the flow, the termination operation can be divided into short-circuit operation and non short-circuit operation.

1 non short circuit operation

1 forEach´╝îforEachOrdered(Consumer action)

  • For parallel operations of streams, the processing results of forEach() are unordered,
  • For parallel operations of streams, the results of forEachOrdered() processing are ordered
    // For parallel operations of streams, the results of forEach() processing are unordered
    @Test
    public void test01() {
        String words = "Beijing is the capital of China";
        Stream.of(words.split(" "))
                .parallel()
                .forEach(System.out::println);
    }

    // For parallel operations of streams, the results of forEachOrdered() processing are ordered
    @Test
    public void test02() {
        String words = "Beijing is the capital of China";
        Stream.of(words.split(" "))
                .parallel()
                .forEachOrdered(System.out::println);
    }

2 collect(Collector col)

Collect the final data from the stream into a collection. The parameter is the Collector collector. The Collector object can be obtained through the static methods of Collectors, such as toList(), toSet().

    @Test
    public void test03() {
        String words = "Beijing is the capital of China";
        List<String> list = Stream.of(words.split(" "))
                .collect(Collectors.toList());
        System.out.println(list);
    }

3 toArray()

Collect the final data in the stream into an array.

4 count()

Count the number of elements in the stream.

5 reduce(BinaryOperator bo)

Reduce, reduce, reduce. The function of this method is to finally convert the collection stream into a specified type of data. The parameter is binary interface BinaryOperator, that is, two inputs and one output, and the type is the same. From two inputs to one output, the effect of reducing reduce is achieved.

    // Calculates the sum of the lengths of all words in the stream
    @Test
    public void test06() {
        String words = "Beijing is the capital of China";
        Optional<Integer> reduce = Stream.of(words.split(" "))
                .map(word -> word.length())
                .reduce((s1, s2) -> s1 + s2);
        // Optional's get() method throws an exception when its encapsulated object is empty
        System.out.println(reduce.get());  // The result is 26
        // When the encapsulated object is empty, the value specified by the parameter is returned
        System.out.println(reduce.orElse(-1));
    }

6 max(Comparator com)

7 min(Comparator com)

2 short circuit operation

1 allMatch (predicate p)

It is used to judge whether all elements meet the specified conditions. As long as one does not meet, the matching will be ended immediately and false will be returned. true will be returned only if all are matched.

2 anyMatch (predicate p)

It is used to judge whether there are qualified elements. As long as a qualified element is found, the matching ends immediately and returns true.

3 noneMatch(predicate p)

It is used to judge whether all conditions are not met. As long as one is found, false will be returned immediately. true will be returned only if all do not match.

4 findFirst() and findAny()

Both the findFirst() and findAny() methods encapsulate the elements found from the stream into the Optional container object. If it is not found, the value in the Optional container object will be empty, and the return value of the isPresent() method of the Optional object will be false.

6 collector

The collect() method of Stream can collect the final data in the Stream into a collection, which is a non short circuit termination operation.
The parameter is Collector collector. Generally, the Collector object is obtained by calling the static method of Collectors. However, the function of the Collector is much more powerful than collecting data into a collection. It can also perform statistics, grouping and other operations on the collected data.
Call different static methods of Collectors according to different requirements to obtain different Collectors.

1 Rev set

toList()

toSet()

toCollection(supplier sup)

By default, the collector uses an unordered HashSet. To specify the use of an ordered TreeSet, you can specify it through the toCollection() method.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String name;
    private String school;
    private String gender;
    private int age;
    private double score;
}

    // Get a list of all participating institutions
    @Test
    public void test03() {
        Set<String> schools = students.stream()
                .map(Student::getSchool)
                .collect(Collectors.toCollection(TreeSet::new));
        System.out.println(schools);
    }

2 grouping

  • Grouping refers to grouping according to the attribute values of the elements specified in the flow.
  • Statistics is the statistics of the number of elements, so there is no need to specify attributes for statistics.
    // Get students from each school (grouped by school)
    @Test
    public void test04() {
        Map<String, List<Student>> schoolGroup = students.stream()
                .collect(Collectors.groupingBy(Student::getSchool));

        // Poor readability of display format
         System.out.println(schoolGroup);

        // Use the tool class to display the key value in the map
        MapUtils.verbosePrint(System.out, "school", schoolGroup);

        // Get the value whose key is "Tsinghua University", that is, get all the contestants from Tsinghua University
        System.out.println(schoolGroup.get("Tsinghua University"));
    }

    // Count the number of contestants from each school
    @Test
    public void test05() {
        Map<String, Long> schoolCount = students.stream()
                .collect(Collectors.groupingBy(Student::getSchool,
                                               Collectors.counting())
                        );

        System.out.println(schoolCount);

        // Number of players from Tsinghua University
        System.out.println(schoolCount.get("Tsinghua University"));
    }

    // All contestants are grouped according to gender
    @Test
    public void test06() {
        Map<String, List<Student>> genderGroup = students.stream()
                .collect(Collectors.groupingBy(Student::getGender));

        MapUtils.verbosePrint(System.out, "Gender", genderGroup);

        // Get all boys
        System.out.println(genderGroup.get("male"));
    }

3 Boolean blocking

Boolean blocks are grouped according to the true and false results of specified assertions. They can only be divided into two groups, and the key can only be true or false.
After blocking, average the contents of the two blocks according to the specified attributes.

    // All contestants are grouped according to gender
    @Test
    public void test07() {
        Map<Boolean, List<Student>> genderGroup = students.stream()
                .collect( Collectors.partitioningBy(s -> "male".equals(s.getGender())) );

        MapUtils.verbosePrint(System.out, "Gender", genderGroup);

        // Get all boys
        System.out.println(genderGroup.get(true));
    }

    // Take 95 as the standard, divide all contestants into groups according to their scores, and divide them into groups greater than 95 and groups not greater than 95
    @Test
    public void test08() {
        Map<Boolean, List<Student>> genderGroup = students.stream()
                .collect(Collectors.partitioningBy(s -> s.getScore() > 95));

        MapUtils.verbosePrint(System.out, "95 Division score", genderGroup);

        // Obtain all students with grades greater than 95
        System.out.println(genderGroup.get(true));
    }

    // Take 95 as the standard, divide all contestants into groups according to their scores, and divide them into groups greater than 95 and groups not greater than 95
    // The average scores of the two groups were calculated respectively
    @Test
    public void test09() {
        Map<Boolean, Double> scoreGroupAvg = students.stream()
                .collect(Collectors.partitioningBy(s -> s.getScore() > 95,
                                                   Collectors.averagingDouble(Student::getScore))
                        );

        System.out.println(scoreGroupAvg);

        // Obtain the average score of all students with a score greater than 95
        System.out.println(scoreGroupAvg.get(true));
    }

4. Obtain summary statistics

Summary statistics refers to the maximum, minimum, average and other data after a certain data in the convection is summarized. This method can only be applied to numerical data statistics.

    // Obtain performance related statistics
    @Test
    public void test10() {
        DoubleSummaryStatistics scoreSummary = students.stream()
                .collect(Collectors.summarizingDouble(Student::getScore));

        System.out.println(scoreSummary);
        // Number of output grades
        System.out.println("Number of scores:" + scoreSummary.getCount());
        // Minimum value in output grade
        System.out.println("Minimum score:" + scoreSummary.getMax());

    }

Topics: Java data structure