Streams allow you to mass-manipulate sequences of data through the mechanisms of mapping, reduction and filtering. For this purpose, since Java 8 you can use the interface public interface Stream<T>
located in the package java.util.stream
.
Operations on a stream are appended into a pipeline, where each operation uses the results of the previous operation as arguments and passes something to the next operation in the pipeline. A simple example should make things clear:
int a[]={1,5,7,10,-1}; Arrays.stream(a).filter(x->x>0).map(x->x*2).reduce(0,(x,y)->x+y);
The handy function Arrays.stream
takes an array and returns a stream, on which we invoke the following operations:
x->x>0
is a shorthand for x->{ return x>0; }
which can be used if you have only one statement inside your lambda expression. In case of the filter operation, our lambda is an implementation of Predicate
which "takes" any type and "returns" a boolean (as you know, we actually implement the function of the Predicate interface). Be aware: The functionality of chaining the remaining elements together is completely governed by the Stream
; the Predicate is just an encapsulation of the behaviour we want to achieve. Do you love streams already?reduce
returns this: 0+2+10+14+20 = 46. This is a terminal operation: it does not return another stream, but - in our case - a number.To understand the mechanisms that come into play here, let's thoroughly analyze the signature of the map operation:
public <R> Stream<R> map(Function<? super T,? extends R> mapper)
Function
, which is a functional interface with the single method: public R apply(T t)
. The Function interface accepts any supertype of T (or T itself) as input parameter type, and will return a subtype of R (or R itself). (You will recall that T is the type of the Stream that is currently operated on.) These two special generics are called bounded wildcards; you can pass in any type as long as it is within the given boundary.List<? extends Number>
could operate on an actual Number
, but also on the more specialized variant Integer
. Sometimes, if you want to lift restrictions on your generic types or methods, think about giving them a bounded wildcard.map
operation will collect the results into a Stream of type R. This requires another generic specialty: the generic method. As you can see above, the declaration contains a generic modifier <R>
which provides that the method returns the same R that the Function interface returned. If this generic method modifier was not used, you could not ensure that these two parties are using the same type as their type parameter.Other noteworthy stream operations are max
, min
, forEach
. Another option instead of creating a stream from an array is Stream.of(1,5,7,10,-1)
.
Streams make use of generics and lambdas, which you have discovered in earlier guides, for a very elegant way of manipulating data. Instead of iterating through an array multiple times, you could create a stream from the source array, perform some operations on the data, and then produce the target array with toArray
. See another example:
public class Person { public Person() { id=-1; name="NULL"; position=0; salary=0; } int id; String name; int position; double salary; } //in main: String csv = Arrays.stream(persons) .filter(x->x.position==MANAGEMENT) .map(x->{ x.name="SCRAMBLED"; return x; }) .map(x->x.id+","+x.salary+"\n") .reduce("ID,SALARY\n",(x,y)->x+y); //alternative 1: will return one large String /*.collect(()->new ArrayList<String>(),(c,x)->c.add(x),(c1,c2)->c1.addAll(c2));*/ //alternative 2: will return an ArrayList of Strings
In this neat example we collect all the management workers into a single CSV string containing their ID and salary. There is no need for countless for loops and accumulator variables; our persons
array remains untouched and we have removed all boiler-plate code.
To extend on the use cases of functional interfaces, let us contemplate one last example:
public class Tree<T> { public Tree left,right; public T value; public <T> ArrayList<T> inOrder(Function<T,T> p1,ArrayList<T> p2) { if(value==null) return new ArrayList<T>(); else { ArrayList<T> a=p2.addAll(left.inOrder(p1,p2)); ArrayList<T> b=a.add(p1.apply(value)); ArrayList<T> c=b.addAll(right.inOrder(p1,b)); return c; } } }
What happens here is that we do a depth-first in-order tree traversal which will store the result of applying the Function p1
to the current node's value in an ArrayList. The inOrder
method operates on a generic type T
, as does the given Function. You can supply any Function to the inOrder
method to allow the reusability of the traversal. For example, to sum all the values in a tree: tree.inOrder(x->x,new ArrayList<Integer>()).stream().reduce(0,(x,y)->x+y);
. And so we have come back to streams... Even if you decide that you would rather do it the old-fashioned way, you have now gained the valuable skill of understanding a different design paradigm.