The stream API is a step in the field of functional programming, that is a different programming style that is more declarative. It is actually much more similar to SQL than to OOP. At any rate, Java introduced the Stream API
and the lambda
to go in this direction.
A stream
is a pipeline to elaborate data. It provides the normal control-flow mechanism in a different way.
A stream
is characterized by 3 elements:
The beginning of the pipeline must provide the data. Streams can be obtained in a number of ways. Some examples include:
Collection
via the stream()
and parallelStream()
methods;Arrays.stream(Object[]);
Stream.of(Object[])
, IntStream.range(int, int)
or Stream.iterate(Object, UnaryOperator)
;BufferedReader.lines()
;Files
;Random.ints()
;BitSet.stream()
, Pattern.splitAsStream(java.lang.CharSequence)
, and JarFile.stream()
.Some examples:
String[] stringArr = {"a", "b", "c", "d"}; Stream<String> stream = Arrays.stream(stringArr); // from arrays Collection<String> list = new Arrays.asList("one" , "two" , "three"); Stream<String> s1 = list.stream(); // from collections Stream<String> s2 = Stream.generate(()->"generate"); // a continuos stream generated by a Supplier // functional interface with get abstract method Stream<String> s3 = Stream.iterate("0" , i -> ++(char)i); Stream<Integer> s4 = Stream.iterate(0, i-> ++i); // generates or better iterates from natural numbers. // It Autoboxes so it does not perform well. // Iterating on numbers it's better // done with appropriate primitive streams. IntStream is5 = IntStream.iterate(0, i -> ++i); // notice that the lambda has a pre-increment otherwise an // infinite stream of 0-s is generated IntStream s6 = IntStream.iterate('0', x -> ++x); s6.limit(25).mapToObj(x->((char)x)).forEach(x->System.out.print(x + " "));
From javadoc:
Intermediate operations return a new stream. They are always lazy; executing an intermediate operation such as filter() does not actually perform any filtering, but instead creates a new stream that, when traversed, contains the elements of the initial stream that match the given predicate. Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed.
A stream is lazily created and executed. So it won't work unless you terminate it. Termination change the stream to something more usable like a List or a Number or whatever you need. Most common terminations are:
collect()
, it collects the result of the stream into a mutable objects as Collectionreduce()
, reduce the stream to a single object based on some rules defined with functional interfaces.Predicate<? super T>
to determine the match.allMatch()
, returns whether all elements of this stream match the provided predicate. May not evaluate the predicate on all elements if not necessary for determining the result.anyMatch()
, returns whether any elements of this stream match the provided predicate. May not evaluate the predicate on all elements if not necessary for determining the result. If the stream is empty then false is returned and the predicate is not evaluated. noneMatch()
, returns whether no elements of this stream match the provided predicate. May not evaluate the predicate on all elements if not necessary for determining the result.findAny()
, returns an Optional
describing some element of the stream, or an empty Optional
if the stream is empty.findFirst()
, returns an Optional
describing the first element of this stream, or an empty Optional if the stream is empty. If the stream has no encounter order, then any element may be returned. forEach()
, iterates on the elements of the stream allowing some operation to be done, but does not return. Hangs on infinite streams. min()
max()
, determine the min and the max value in the stream according to an optional comparator. Return an Optional. Hangs on infinite streams.count()
, determines the number of elements in the stream. Hangs for infinite streams.The signature is
T reduce(T identity, BinaryOperator<T> accumulator)
It uses a BinaryOperator
functional interface to accumulate the result, by setting an initial value (identity
). It has 2 inputs and 1 output of the same type. You can use it, for example to calculate the pi=3.14…
with the Gregory-Leibniz series
import java.util.*; import java.util.function.Supplier; import java.util.stream.*; public class GregoryLeibnitz { public static void main(String[] args) { int limit = 1000; Double pi = 4 * IntStream.iterate(0, n -> n+1) .limit(limit) // limit the infinite stream .mapToDouble(v -> (double)v) .reduce(0., //identity, i.e. the starting point (a,x) -> a + (x%2==0?1:-1) / (2*x+1) //BinaryOperator<Double> ); System.out.println("pi= " + pi); // 3.140592653839794 } }
There is another signature:
Optional<T> reduce(BinaryOperator<T> accumulator)
by which you do not pass the identity
and you don't get the right type T
but an Optional<T>
. In this case the first value in the accumulator is the first value passed to it.
OptionalInt sum = IntStream.iterate(1, n->n+1) .limit(3) .reduce((a,x) -> a + x); System.out.println("sum " + sum.toString()); // sum OptionalInt[6] System.out.println("sum " + sum.getAsInt()); // sum 6 // notice that it sums in this way: // x=1 a=1 // first value is the first element // x=2 a=1+2 // x=3 a=3+3
A last option is:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Performs a reduction on the elements of this stream, using the provided identity, accumulation and combining functions. Generally used with parallel streams, it is like splitting the reduction in 2 steps. The identity value must be an identity for the combiner function. This means that for all u, combiner(identity, u) is equal to u. Additionally, the combiner function must be compatible with the accumulator function; for all u and t, the following must hold:
combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
The javadoc is pretty explanatory of the first signature:
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
This produces a result equivalent to:
R result = supplier.get(); for (T element : this stream) accumulator.accept(result, element); return result;
So with the supplier
you create an object where to store the result (e.g. an ArrayList), the accumulator calls the functional interface that is a BiConsumer
and gets 2 elements and combines them (e.g. .add()
in an ArrayList). Lastly, if the operation is performed in parallel the combiner combines the result together in another type R
container by using its accept
method.
For example, the following will accumulate strings into an ArrayList:
List<String> asList = stringStream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
The following will take a stream of strings and concatenates them into a single string:
String concat = stringStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append) .toString();
import java.util.*; import java.util.stream.*; public class StreamExercise { public static void main(String[] args) { // calculate the sum of an array List<Integer> list = Arrays.asList(0,1,2,3); int thesum = list.stream() //create the stream .reduce(0,(a,x)->a+x); // elaborate and return System.out.println(thesum); // 6 // data from a Supplier Stream<Integer> s = Stream.iterate(0, n -> n+1); Integer i = s.limit(4) .reduce(0, (a,x) -> a + x ); System.out.println(i); // 6 } }