An intro to Java8 lambdas and streams

In this post we are going to walk through a brief introduction to Lambdas and streams in Java8. Let's begin by transforming an anonymous inner class into a lambda expression and finally a method reference.


    class Example {

      public void example(){

         Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
               System.out.println("Hello from the anonymous inner class");
            }
         };

         myRunnable.run();
  
         Runnable myVerboseLambdaRunnable =
            () -> { System.out.println("Hello from a lambda"); };

         Runnable myLambdaRunnable = 
            () -> System.out.println("Hello from a lambda");


         myVerboseLambdaRunnable.run();
         myLambdaRunnable.run();
   

         Runnable myMethodReference = this::sayHello;
         myMethodReference.run();
      }

      private void sayHello(){
         System.out.println("Hello from a method reference");
      }
    }

Lambdas are especially useful when we are working with a "functional interface", an interface with one abstract method. The java language includes many functional interfaces but they can all be broken into one of these four categories:

  • Supplier: takes no input but will return an object
  • Consumer: takes an object as input but returns nothing
  • Predicate: takes an object and returns a boolean
  • Function: takes an object and returns an object

    //A Supplier
    Supplier<String> stringSupplier = new Supplier<String>() {
       @Override
       public String get() {
         return "Hello World";
       }
    };

    //as a Lambda
    Supplier<String> lambdaSupplier = () -> "Hello World";

    //A Consumer
    Consumer<String> stringConsumer = new Consumer<String>(){
       @Override
       public void accept(String s){
         System.out.println(s);
       }
    };

    //as a lambda
    Consumer<String> lambdaConsumer = s -> System.out.println(s);

    //A Predicate
    Predicate<String> stringPredicate = new Predicate<String>() {
       @Override
       public boolean test(String s) {
         return "Hello World".equals(s);
       }
    };

    //as a lambda
    Predicate<String> lambdaPredicate = s -> "Hello World".equals(s);

    //A Function
    Function<String,String> stringFunction =
       new Function<String, String>() {
           @Override
           public String apply(String s) {
             return new StringBuilder(s).reverse().toString();
           }
        };

    Function<String,String> lambdaFunction =
       s -> new StringBuilder(s).reverse().toString();

Now that we've seen how to create some simple lambdas, let's take a look at streams.
Streams in Java8 make it easy for us to process data clearly and concisely. In general, your workflow when using streams will be to perform some filtering or mapping operations followed by a reduce operation of some kind. Let's take a look at a simple example before we dive a little deeper.

        List<String> wordList = new ArrayList<>();

        wordList.add("Hello");
        wordList.add("Stream");
        wordList.add("World");

        wordList.stream()
                .forEach(System.out::println);

We create a stream by calling the .stream() method. The forEach() method will then process each element in our stream one at a time.
The stream can only be "executed" a single time. If we wanted to process the data again, we'd need to create a new stream. That's ok though because they are very light weight objects. A key thing to notice in our example is that we had a "terminating" step. A stream will not execute unless it has a terminating step.
In the example below our stream does not have a terminating step so our list will remain empty.

        Integer[] numbers = new Integer[]{1,2,3,4};
        List<Integer> evenNumbers = new ArrayList<>();
        Stream.of(numbers)
                .filter(integer -> integer % 2 == 0)
                .peek(evenNumbers::add);

        System.out.println(evenNumbers.size());//will print 0


Our list of even numbers is still empty because filter() and peek() are not
terminating operations on a stream. They will not evaluate until a terminating operation is performed on the stream. If we switched peek() to forEach() like we did in our previous example, our list would contain all of the even numbers in our stream.

We've seen an example using filtering, so now let's look at the mapping operations we can use with streams. First, a simple map operation:

        Integer[] numbers = new Integer[]{1,2,3,4};
        List<String> stringRepresentation = new ArrayList<>();
        Stream.of(numbers)
                .map(integer -> String.valueOf(integer))
                .forEach(stringRepresentation::add);

        System.out.println(stringRepresentation.size());//will print 4


We start with an array of Integers but map them into a list of Strings. Filtering and Mapping allow us to transform and change a stream as it flows through each step. These operations can be very powerful tools when you need to manipulate your data in some way.

We can also perform a flatmap operation, which will take an object from our initial stream and perform a mapping operation that will output a stream of its own. This is different from a normal map operation because instead of one input object producing one output object, a flatmap can produce n output objects from a single input.

        Integer[] even = new Integer[]{2,4,6};
        Integer[] odd = new Integer[]{1,3,5};

        List<Integer> allNumbers = new ArrayList<>();

        Stream.of(even,odd)
                .flatMap(integers -> Arrays.stream(integers))
                .forEach(allNumbers::add);


        System.out.println(allNumbers.size());//will print 6


We start with a stream that consists of two integer arrays, but our flatmap operation will turn each of those arrays into a stream of their own. Our stream of two integer arrays was transformed by the flatmap operation into a stream of 6 integers. We then took each of those integers and added them to our list object to complete the operation.

Now let's take a closer look at the "reduction" steps of streams. There are two types of reduction that we can perform on our streams:

  1. Aggregation
  2. Collector
        Integer[] numbers = new Integer[]{1,2,3,4,5};

        Integer sumOfIntegers = Arrays.stream(numbers)
                .reduce(0, (first, second) -> first + second);

        //will print 15
        System.out.println(sumOfIntegers);

In the aggregation example, we used 0 as our "identity" value. This value
is the initial seed value for the accumulator lambda that we provided. It is also the default value returned if there are no values in our stream. 0 worked for us in this case, but for cases where a default value does not make sense, you can simply pass your accumulator lambda and get back an Optional.


        Integer[] numbers = new Integer[]{1,2,3};
        Integer[] emptyArray = new Integer[0];

        BinaryOperator<Integer> biFunction = Math::max;
        Optional<Integer> maxOfNumbers = Arrays.stream(numbers)
                .reduce(biFunction);

        //will print 3
        System.out.println(maxOfNumbers.get());

        Optional<Integer> emptyMax = Arrays.stream(emptyArray)
                .reduce(biFunction);

        //will print false
        System.out.println(emptyMax.isPresent());

In this example we are looking for the max value in our integer arrays.However, one of our integer arrays is empty, and we don't have a good value for an "identity" for the max function. Should an empty array of integers really have a max value of 0? Or of Integer.MIN? We can handle this by simply passing our bifunction to reduce() and then letting it return an optional to us.

Now that we've covered aggregation, let's look at the collectors. The collectors allow us to process a stream of data and dump the results into a single data structure that we can then use elsewhere in our application.
In this example, we will collect our stream elements into a map.

        Car[] cars = new Car[]{
                new Car("Chevrolet", "Cavalier"),
                new Car("Mazda", "3"),
                new Car("Ford", "F150"),
                new Car("Chevrolet", "Volt")
        };

        Map<String, List<Car>> makeToCarsMap = Arrays.stream(cars)
                .collect(Collectors.groupingBy(Car::getMake));

        //will print 2
        System.out.println(makeToCarsMap.get("Chevrolet").size());
        

We use Collectors.groupingBy to provide our map's key and then let the Collector magic create a map and place each item into that map according to the groupingBy value. In our case, we will group the car objects by their make. In then end, we will have a map that will take us from a make to a list of models.

That concludes our introduction to lambdas and streams. Here are a few links that I found helpful when learning about lambdas and streams in Java8.