One of the highlights of Java 8 is that it is the next update to Java 5 on collections. The Streams API was designed as a functional protagonist of Java.

What is a Stream

It works like a bottle of water into a pipe with many filter valves. Each time the water passes through one of the filter valves, it is processed, filtered, converted, etc. Finally, a container at the other end of the pipe receives the rest of the water.

The schematic diagram is as follows:

The flow is first generated through the source, followed by intermediate operations such as filtering, transformation, restriction, and so on, and finally completes the operation of the flow.

Stream can also be understood as a more advanced iterator whose main function is to iterate through each element.

Why do WE need Stream?

Stream, as one of the highlights of Java 8, provides various convenient, simple and efficient apis specifically for all kinds of collection operations. The Stream API is mainly completed through Lambda expressions, which greatly improves the efficiency and readability of the program. At the same time, the Stram API’s own parallel Stream makes the threshold of concurrent processing set lower again, using the Stream API programming can be very convenient to write high performance concurrent programs without writing more than one line of multithreading door. Using the Stream API can make your code more elegant.

Another feature of streams is that they can be infinite. With a Stream, your data source can be infinite.

Before there is no Stream, we want to extract all students older than 18, we need to do this:

List<Student> result=new ArrayList<>();
for(Student student:students){
 
    if(student.getAge()>18){ result.add(student); }}return result;
Copy the code

Using Stream, we can follow the process diagram above, first generating Stream, then filtering, and finally merging into the container.

The conversion code is as follows:

return students.stream().filter(s->s.getAge()>18).collect(Collectors.toList());
Copy the code
  • First of all,stream()Get the flow
  • thenfilter(s->s.getAge()>18)filter
  • The lastcollect(Collectors.toList())Merge into the container

Is it like writing SQL?

How to use Stream

We can see that when we use a stream, there are three main steps:

  • Access to flow
  • Operate by convection
  • End the convection operation

Access to flow

There are several ways to fetch streams. For common collections, you can fetch streams directly. Stream ()

  • Collection.stream()
  • Collection.parallelStream()
  • Arrays.stream(T array) or Stream.of()

For IO, we can also get streams through the lines() method:

  • java.nio.file.Files.walk()
  • java.io.BufferedReader.lines()

Finally, we can generate streams from infinitely large data sources:

  • Random.ints()

It’s worth noting that expensive boxing and unboxing operations for primitive data types in the JDK provide streams for primitive data types:

  • IntStream
  • LongStream
  • DoubleStream

These three basic data types are similar to normal streams, except that the data in each stream is the specified basic data type.

Intstream.of(new int[] {1.2.3});
Intstream.rang(1.3);
Copy the code

Operate by convection

This is the focus of this chapter. Generating streams is relatively easy, but there are many different requirements for different business systems. Understanding what we can do with streams and how we can do it will take advantage of the features of the Stream API.

There are two types of operations for streams:

  • Intermediate: Intermediate operations. A stream can be followed by zero or more Intermediate operations. The main purpose is to open the stream, do some degree of data mapping/filtering, and then return a new stream for the next operation to use. These operations are lazy, meaning that they are called without actually starting the flow.

    Map (mapToInt, flatMap, etc.), filter, distinct, sorted, peek, limit, Skip, parallel, sequential, and unordered

  • Terminal: terminates a stream. Only one Terminal operation can be performed on a stream. After this operation is performed, the stream is used as light and cannot be operated again. So this must be the last operation of the stream. The execution of the Terminal operation will actually start the stream traversal and will produce a result, or side effect.

    ForEach, forEachOrdered, toArray, Reduce, collect, min, Max, count, anyMatch, allMatch, noneMatch, findFirst, findAny, iterator

Intermediate and Terminal can be fully understood according to the flow chart in the figure above. Intermediate refers to the filter in the middle of the pipe, where water flows into the filter and then flows out. Terminal operation is the last filter, which is at the end of the pipe and flows into the water of Terminal. Eventually it will flow out of the pipe.

Here is a detailed explanation of the effect of each operation:

In the middle of operation

For intermediate operations, the return value of all apis is basically Stream

, so you can determine the type of an unfamiliar API by its return value.

map/flatMap

A map, as the name implies, is a map, and the map operation maps each element in the flow to another element.

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

As you can see, the map accepts a Function, that is, a parameter, and returns a value.

Such as:

// Fetch List
      
        all Student names
      
List<String> studentNames = students.stream().map(Student::getName)
                                             .collect(Collectors.toList());
Copy the code

The code above is the same as before:

List<String> studentNames=new ArrayList<>();
for(Student student:students){
    studentNames.add(student.getName());
}
Copy the code

Convert all letters in the List to uppercase:

List<String> words=Arrays.asList("a"."b"."c");
List<String> upperWords=words.stream().map(String::toUpperCase)
                                      .collect(Collectors.toList());
Copy the code

FlatMap, as its name implies, is a flat mapping. Its specific operation is to connect multiple streams into one stream. This operation is similar to multi-dimensional array, such as container containing container, etc.

List<List<Integer>> ints=new ArrayList<>(Arrays.asList(Arrays.asList(1.2),
                                          Arrays.asList(3.4.5)));
List<Integer> flatInts=ints.stream().flatMap(Collection::stream).
                                       collect(Collectors.toList());
Copy the code

As you can see, it’s dimension reduction.


filter

A filter, as the name implies, is a filter. The elements that pass the test are left behind and a new Stream is generated

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

Similarly, a filter can take parameters like Predicate, which is the Predicate function interface, and return a Boolean.

Such as:

// Get all students older than 18
List<Student> studentNames = students.stream().filter(s->s.getAge()>18)
                                              .collect(Collectors.toList());
Copy the code

distinct

Distinct is a deduplication operation and has no arguments

  Stream<T> distinct(a);
Copy the code

sorted

The sorted method contains an overload. If no arguments are passed, the sorted elements in the stream need to implement the Comparable

method. You can also pass in a Comparator

when using the sorted method.

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

Stream<T> sorted(a);
Copy the code

It’s worth noting that this Comparator was tagged with @functionalInterface after Java 8. All the other methods provide a default implementation, so we can use Lambda expressions in sort

Such as:

// Sort by age
students.stream().sorted((s,o)->Integer.compare(s.getAge(),o.getAge()))
                                  .forEach(System.out::println);;
Copy the code

By Comparator’s default, the method reference is implemented to make it easier to use:

For example, the above code could be changed to the following:

// Sort by age
students.stream().sorted(Comparator.comparingInt(Student::getAge))
                            .forEach(System.out::println);;
Copy the code

Or:

// Sort by name
students.stream().sorted(Comparator.comparing(Student::getName)).
                          forEach(System.out::println);
Copy the code

Is it more concise?


peek

Peek means traversal, like forEach, but it is an intermediate operation.

Peek accepts a consumptive functional interface.

Stream<T> peek(Consumer<? super T> action);
Copy the code

Such as:

// Print it out and merge it into List
List<Student> sortedStudents= students.stream().distinct().peek(System.out::println).
                                                collect(Collectors.toList());
Copy the code

limit

String::subString(0,x) : String::subString(0,x) : String::subString(0,x) : String::subString(0,x) : limit takes a long argument

 Stream<T> limit(long maxSize);
Copy the code

Such as:

// Just leave the first six elements and print
students.stream().limit(6).forEach(System.out::println);
Copy the code

skip

Skip means how many elements to skip, which is similar to limit, but limit means to keep the preceding elements, skip means to keep the following elements

Stream<T> skip(long n);
Copy the code

Such as:

// Skip the first three elements and print
students.stream().skip(3).forEach(System.out::println);
Copy the code

Put an end to the operation

In a stream, there is only one terminating operation, after which the stream is processed. The terminating operation usually returns a type other than a stream. In general, the terminating operation converts it to a container.

forEach

ForEach is a traversal of the terminating operation, which is the same as PEEK, but no stream is returned after forEach

 void forEach(Consumer<? super T> action);
Copy the code

Such as:

// Iterate over the print
students.stream().forEach(System.out::println);
Copy the code

The above code has the same effect as the following:

for(Student student:students){
    System.out.println(sudents);
}
Copy the code

toArray

ToArray is similar to List##toArray(), including an overload.

The default toArray() returns an Object[],

You can also pass an IntFunction

generator to specify the data type
[]>

The second method is generally recommended.

Object[] toArray();

<A> A[] toArray(IntFunction<A[]> generator);
Copy the code

Such as:

 Student[] studentArray = students.stream().skip(3).toArray(Student[]::new);
Copy the code

max/min

Max /min Even to find the largest or smallest element. Max /min must be passed a Comparator.

Optional<T> min(Comparator<? super T> comparator);

Optional<T> max(Comparator<? super T> comparator);
Copy the code

count

Count returns the number of elements in the stream

long count(a);
Copy the code

Such as:

long  count = students.stream().skip(3).count();
Copy the code

reduce

Reduce is an inductive operation that combines elements ina stream. It provides a starting value and performs operations according to certain rules, such as adding. It receives a BinaryOperator function interface. In a sense, sum,min, Max and average are special reduce

Reduce contains three overloads:

T reduce(T identity, BinaryOperator<T> accumulator);

Optional<T> reduce(BinaryOperator<T> accumulator);

 <U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);
Copy the code

Such as:

List<Integer> integers = new ArrayList<>(Arrays.asList(1.2.3.4.5.6.7.8.9.10));
        
long count = integers.stream().reduce(0,(x,y)->x+y);
Copy the code

The above code is equivalent to:

long count = integers.stream().reduce(Integer::sum).get();
Copy the code

The difference between the two reduce parameters and the one reduce parameter is whether a starting value is provided.

A definite value can be returned if a starting value is provided, or Opeational in case there are not enough elements in the flow if not.


anyMatch\ allMatch\ noneMatch

Tests if any element/all elements/no element matches the expression

They both receive a functional interface to infer types: Predicate

 boolean anyMatch(Predicate<? super T> predicate);

 boolean allMatch(Predicate<? super T> predicate);

 boolean noneMatch(Predicate<? super T> predicate)
Copy the code

Such as:

 boolean test = integers.stream().anyMatch(x->x>3);
Copy the code

FindFirst, findAny

Either API takes no arguments, findFirt returns the first element in the stream, and findAny returns any element in the stream.

Optional<T> findFirst(a);

Optional<T> findAny(a);
Copy the code

FindAny () is a weird operation. Who would use it? The main purpose of this API is to get arbitrary elements in parallel for maximum performance

Such as:

int foo = integers.stream().findAny().get();
Copy the code

collect

Collect operation, this API will come later because it is so important that basically all stream operations end up using it.

Let’s first look at the definition of collect:

 <R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);

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

Collect contains two overloads:

One parameter and three parameters,

We rarely use these three parameters because the JDK provides enough of a Collector that we can use directly.

  • Supplier: The producer used to produce the container that finally holds the element
  • accumulator: Method to add elements to a container
  • combiner: Adds all the segmented elements to the container

The first two elements are well understood, but what is the third element? Because streams provide parallel operations, it is possible for a stream to be added separately by multiple threads, and then each sublist in turn is added to the final container.

↓ – – – – – – – – –

Left — — — — — — — — —

Left — — — — — — — — —

Divide and conquer, as shown above.

Such as:

List<String> result = stream.collect(ArrayList::new, List::add, List::addAll);
Copy the code

Next look at collect, which has only one argument

Generally, a method reference in Collectors can be passed directly to collect, which has only one parameter:

List<Integer> = integers.stream().collect(Collectors.toList());
Copy the code

Collectors contain many common converters. ToList (), toSet (), etc.

Collectors also includes groupBy(), which, like groupBy in Sql, is a grouping and returns a Map

Such as:

// Group students by age
Map<Integer,List<Student>> map= students.stream().
                                collect(Collectors.groupingBy(Student::getAge));
Copy the code

GroupingBy can accept three parameters, namely

  1. The first parameter: How are the groups classified
  2. Second argument: what container is used to store the return of the group (when only two arguments are used, this parameter defaults toHashMap)
  3. Third parameter: after classifying according to the first parameter, how to collect the result of corresponding classification

Sometimes when the single-parameter groupingBy does not meet our requirements, we can use the groupingBy with multiple parameters

Such as:

// Group the students by age. In each group, only the students' names are stored, not the objects
Map<Integer,List<String>> map =  students.stream().
  collect(Collectors.groupingBy(Student::getAge,Collectors.mapping(Student::getName,Collectors.toList())));
Copy the code

ToList generates ArrayList by default, and toSet generates HashSet by default. If you want to specify other containers, you can do the following:

 students.stream().collect(Collectors.toCollection(TreeSet::new));
Copy the code

Collectors also includes a toMap, which can be used to convert a List to a Map

  Map<Integer,Student> map=students.stream().
                           collect(Collectors.toMap(Student::getAge,s->s));

Copy the code

IntStream, LongStream,DoubleStream, IntStream, LongStream,DoubleStream, IntStream, LongStream,DoubleStream, IntStream, LongStream,DoubleStream, IntStream, LongStream,DoubleStream, IntStream, LongStream,DoubleStream, IntStream, LongStream,DoubleStream, IntStream, LongStream,DoubleStream, IntStream, LongStream,DoubleStream, IntStream, LongStream


Use Stream gracefully

Now that you know about the Stream API, here’s how to use Steam gracefully

  • Understand lazy operations of streams

    As mentioned earlier, the intermediate operations of a flow are lazy. If a flow has only intermediate operations and no finalization operations in the flow, then the flow does nothing, and the process does not actually start executing until the finalization operation is encountered.

    Such as:

    students.stream().peek(System.out::println);
    Copy the code

    Such a stream operation has only intermediate operations and no terminations, so no matter how many elements the stream contains, it will not perform any operations.

  • Understand the importance of the sequence of stream operations

    The Stream API also includes a class of short-circuiting that changes the number of elements in the Stream. This class of API is usually best written early if it is an intermediate operation:

    Consider these two lines of code:

    students.stream().sorted(Comparator.comparingInt(Student::getAge)).
                      peek(System.out::println).
                      limit(3).              
                      collect(Collectors.toList());
    Copy the code
    students.stream().limit(3).
                      sorted(Comparator.comparingInt(Student::getAge)).
                      peek(System.out::println).
                      collect(Collectors.toList());
    Copy the code

    Both pieces of code use the same API, but the result is very different due to the different order,

    The first piece of code will sort all the elements, print them out, and finally get the first three smallest ones and put them in the list,

    The second code intercepts the first three elements, sorts them, prints through them, and puts them in the list.

  • Understand the limitations of Lambda

    Since Java is currently only pass-by-Value, lambdas have the same final limitations as anonymous classes.

    For specific reasons, see Java Dry goods for a deep understanding of inner classes

    Therefore, we cannot modify the values of external elements in lambda expressions.

    Also, in Stream, we cannot use break to return ahead of time.

  • Format the Stream code properly

    Because you can process a lot of business logic when you use streaming programming, resulting in a very long API, you end up using a newline to separate the operations and make the code more readable.

    Such as:

    students.stream().limit(3).
                      sorted(Comparator.comparingInt(Student::getAge)).
                      peek(System.out::println).
                      collect(Collectors.toList());
    Copy the code

    Instead of:

    students.stream().limit(3).sorted(Comparator.comparingInt(Student::getAge)).peek(System.out::println).collect(Collectors.toList());
    Copy the code

    Also, since Lambda expressions omit parameter types, use complete nouns for variables, such as student rather than s, to increase the readability of the code.

    Write as much code as you dare to leave your name in the code comment!

conclusion

In short, Stream is the magic tool that Java 8 provides for simplifying code, and when used properly, it can make your code more elegant.

Respect the success of labor, reprint to indicate the source

Reference links:

Effective Java 3th

The Streams API in Java 8

Advanced Java Stream API

Check whether the data structure after Java8 stream groupby can be reconstructed


If you think it is well written, welcome to follow the wechat public account: Yiyou Java, every day from time to time to release some articles about Java dry goods, thank you for your attention