Stream basis of functional programming stream in Java

Write in front

If functional interfaces and lambda expressions are the cornerstone of functional programming in Java, stream is the most magnificent building on the cornerstone.

Only when you are familiar with stream can you say you are familiar with Java   Functional programming.

This paper mainly introduces the basic concept and basic operation of Stream, so that we can have a preliminary understanding of Stream.

The sample code for this article is available on gitee: Javafp: sample code for Java functional programminghttps://gitee.com/cnmemset/javafp

Concept of stream

First, take a typical stream example:

public static void simpleStream() {
    List<String> words = Arrays.asList("hello", "world", "I", "love", "you");
    int letterCount = words.stream()
            .filter(s -> s.length() > 3)  // Filter out words with length less than or equal to 3
            .mapToInt(String::length)     // Map each word to word length
            .sum();  // Calculated total length 5(hello) + 5(world) + 4(love) = 14
 
    // The output is 14
    System.out.println(letterCount);
}

In the above example, we will list the strings   words is used as the data source of stream, and then the   The series operation (sum) method of filter map reduce belongs to   reduce   Operations), and map and reduce will be described in detail later   Operation. If you have big data programming experience, it will be easier to understand the meaning of map and reduce.

The definition of stream is rather obscure, which can be roughly understood as a sequence of data elements supporting serial or parallel operations. It has the following characteristics:

  • First, stream is not a data structure, it does not store data. Stream is a data view on a data source. The data source can be an array, a Collection class, or even an I/O channel. It passes through a calculation pipeline   of   computational operations) to perform filter map reduce operations on the data of the data source.
  • Second, stream inherently supports functional programming. An important feature of functional programming is that it does not modify the value of variables (no "side effects"). Any operation on stream will not modify the data in the data source. For example, if you filter a stream whose data source is Collection, only a new stream object will be generated, and the elements in the underlying data source will not be deleted.
  • Third, many operations of stream are lazy looking. Lazy evaluation means that the operation is only a description of the stream and will not be executed immediately. This kind of lazy operation is called intermediate operation in stream   operations).
  • Fourth, the data presented by stream can be infinite. For example, Stream.generate can generate an infinite stream. We can pass   limit(n) method to convert an infinite flow into a finite flow, or   The findFirst() method terminates an infinite stream.
  • Finally, elements in the stream can only be consumed once. And iterators   Iterator   Similarly, when you need to repeatedly access an element, you need to regenerate a new stream.

Stream operations can be divided into two categories: intermediate operations   Operations and terminal   operations). A stream pipeline consists of a data source + 0 or more intermediate operations + 1 termination operation.

Intermediate operation:

intermediate operation   Operations) refer to operations that convert one stream to another, such as filter and map operations. intermediate operations are inert. Their function is only to describe a new stream and will not be executed immediately.

Terminate operation:

Terminate operation   Operations) refer to those operations that will produce a new value or side effect, such as count   and   forEach   Operation. Only when a termination operation is encountered, the previously defined intermediate operation will actually be executed. It should be noted that when a stream performs a termination operation, its status will become "consumed" and can no longer be used.

In order to prove that "intermediate operations are inert", we designed an experimental example code:

public static void intermediateOperations() {
    List<String> words = Arrays.asList("hello", "world", "I", "love", "you");
 
    System.out.println("start: " + System.currentTimeMillis());
 
    Stream<String> interStream = words.stream()
            .filter(s -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // do nothing
                }
                return s.length() > 3;
            });
    IntStream intStream = interStream.mapToInt(s -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // do nothing
        }
        return s.length();
    });
 
    // Because both filter and map operations are intermediate operations and will not be executed,
    // Therefore, they are not affected by Thread.sleep and take a short time
    System.out.println("after filter && map: " + System.currentTimeMillis());
 
    int letterCount = intStream.sum();
 
    // sum is a termination operation and will execute the intermediate operations defined previously,
    // Thread.sleep is actually executed, taking 5(filter) + 3(mapToInt) = 8 seconds
    System.out.println("after sum: " + System.currentTimeMillis());
 
    // The output is 14
    System.out.println(letterCount);
}

The output of the above code is similar:

start: 1633438922526
after filter && map: 1633438922588
after sum: 1633438930620
14

It can be seen that the above code verifies that "intermediate operations are inert": there is only a few tens of milliseconds between printing "start" and printing "after filter & & map", while printing "after"   "Sum" proved that only when it was encountered after 8 seconds   sum   After operation, filter   and   map   The function defined in is actually executed.

Generate a stream object

In Java 8, four stream interfaces are introduced: stream, IntStream, LongStream and DoubleStream, corresponding to Object type and basic types int, long and double respectively. As shown in the figure below:

In Java, stream related operations are basically implemented through the above four interfaces, and specific stream implementation classes are not involved. To get a stream, you usually do not create it manually, but call the corresponding tool method.

Common tools and methods include:

  1. Collection method: Collection.stream() or   Collection.parallelStream()
  2. Array method: Arrays.stream(Object [])
  3. Factory method: Stream.of(Object[]), IntStream.range(int, int), or   Stream.iterate(Object, UnaryOperator)   wait
  4. Read file method: BufferedReader.lines()  
  5. class   java.nio.file.Files also provides Stream related API s, such as   Files.list, Files.walk, etc

Basic operation of Stream

Taking the interface stream as an example, let's first introduce some basic operations of stream.

forEach()

The forEach method in Stream is similar to the forEach method in Collection, which performs the specified operation on each element.

The forEach method signature is:

void forEach(Consumer<? super T> action)

The forEach method is a termination operation, which means that all intermediate operations before it will be executed, and then executed immediately   action  .

filter()

The method signature of the filter method is:

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

The filter method is an intermediate operation. Its function is based on parameters   Predict filter element and return a Stream that only contains elements that meet the predict condition.

Example code:

public static void filterStream() {
    List<String> words = Arrays.asList("hello", "world", "I", "love", "you");
    words.stream()
            .filter(s -> s.length() > 3)  // Filter out words with length less than or equal to 3
            .forEach(s -> System.out.println(s));
}

The above code output is:

hello
world
love

limit()

The limit method signature is:

Stream<T> limit(long maxSize);

The limit method is a short-circuiting intermediate operation. It is used to cut off the current Stream and leave only the most   maxSize   Elements form a new Stream. Short circuiting means converting a Stream of infinite elements into a Stream of finite elements.

For example, Random.ints can generate an approximately infinite stream of random integers. We can limit the number of random integers generated by the limit method. Example code:

public static void limitStream() {
    Random random = new Random();
 
    // Print 5 random integers in [1, 100) in the left closed right open interval
    random.ints(1, 100)
            .limit(5)
            .forEach(System.out::println);
}

The output of the above code is similar:

90
31
31
52
63

distinct()

The method signature of distinct is:

Stream<T> distinct();

distinct is an intermediate operation that returns a Stream after removing duplicate elements.

The author once encountered an interesting scene: to generate 10 non repeated random numbers. This requirement can be realized in combination with the Random.ints method (Random.ints can generate an approximately infinite random integer stream). The example code is as follows:

public static void distinctStream() {
    Random random = new Random();
 
    // In the left closed right open interval [1, 100), 10 non repeated numbers are randomly generated
    random.ints(1, 100)
            .distinct()
            .limit(10)
            .forEach(System.out::println);
 
    /*
    // An interesting question is that if the limit method is placed in front of distinct,
    // Is there any difference between the result and the above code?
    // Welcome to group discussion.
    random.ints(1, 100)
            .limit(10)
            .distinct()
            .forEach(System.out::println);
    */
}

sorted()

There are two signatures for sorted methods:

Stream<T> sorted();

Stream<T> sorted(Comparator<? super T> comparator);

The former is sorted in natural order, and the latter is sorted according to the specified comparator.

The sorted method is an intermediate operation similar to the Collection.sort method.

The example code is as follows:

public static void sortedStream() {
    List<String> list = Arrays.asList("Guangdong", "Fujian", "Hunan", "Guangxi");
 
    // Natural sorting
    list.stream().sorted().forEach(System.out::println);
 
    System.out.println("===============");
 
    // To sort provinces, first sort by length. If the length is the same, sort by alphabetical order
    list.stream().sorted((first, second) -> {
        int lenDiff = first.length() - second.length();
        return lenDiff == 0 ? first.compareTo(second) : lenDiff;
    }).forEach(System.out::println);
}

The output of the above code is:

Fujian
Guangdong
Guangxi
Hunan
===============
Hunan
Fujian
Guangxi
Guangdong

epilogue

Welcome to the functional programming world of Java!!!

This paper introduces the concept and basic operation of Stream, especially the concepts of intermediate operation and termination operation.

After carefully reading this article, you should have a preliminary understanding of Stream, but this is only an introduction to Stream programming. The more interesting, challenging and playable is the map reduce operation to be introduced later.

Tags: Java Back-end Functional Programming

Posted on Mon, 25 Oct 2021 07:36:40 -0400 by westen