Streams

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:

  1. Filtering checks each element of the stream for a given predicate and only passes it to the next operation if the predicate evaluated to true. Here, we drop all numbers that are less than 1. The snippet 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?
  2. The map invokes a given function on each element of the resulting stream. In the example, we multiply each number by 2.
  3. The last step reduces the elements of the stream: if we provide an identity and a binary function, we can melt the stream into one single number. In our example, we provide the identity 0 and the addition function, so that the 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)

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.