1 A Motivating Problem
2 Introducing Streams
3 Converting Between Streams and Lists
4 Other Operations on Streams
5 Practice With Streams
5.1 Rainfall
5.2 Adding Machine
5.3 Shopping Cart
5.4 Sum Over Table
6 What’s So Great About Lambda Expressions?
7 What to Turn In

Map, Filter, Lambdas and Java Streams

Kathi Fisler

If you came through Racket, you’ve already seen filter and map: the former selected those list elements that satisfied a predicate (a function that returns boolean), while the latter applied some function to every element of a list. These patterns are so useful that Java provided support for them starting with Java 8. In this lecture, we learn how to write map- and filter-like expressions in Java.

Those of you who have never been exposed to maps or filters should ask for help with the concepts as needed.

1 A Motivating Problem

Assume you have a LinkedList (say, words) of Strings and want a list of all of the Strings whose length is at least 5. Assume you had a function (say longerThanFive) that took a string and returned a boolean indicating whether the length was longer than five. You want to say "give me the list of strings from Words for which longerThanFive returns true".

In Racket, you would write (filter longerThanFive Words)

To do this in Java, you need to know (a) how to call the filter method on a list, and (b) how to pass the longerThanFive method as an argument. Both involve slight changes to what you have seen before.

2 Introducing Streams

Java doesn’t provide filter and map directly on lists. Instead, these operations work on Streams, a special kind of data for collections of elements, designed to support maps and filters.

So for the moment, let’s assume that you had a Stream of words, rather than a LinkedList of words. We’ll call it wordStream. Now, to get a stream containing just the words from the stream that are longer than 5 characters, we would write the following:

  wordStream.filter(word -> word.length() > 5)

The argument to filter is a shorthand way to specify a predicate (a function that produces a boolean). For those who took CS 1102, the shorthand is simply a lambda expression. Java also calls these lambda expressions, borrowing the common terminology from functional languages.

Now, what if we also wanted to do two things: get the strings that were longer than five characters in length and convert them all to upper case letters. The Java String class has a method called toUpperCase that capitalizes all the letters in a string. So we want to call the toUpperCase method on every string that is in the stream after we filter out the long ones.

This is a job for map. The map method on streams takes a function as input and runs it on every item in the stream, producing a stream of the results. Here are both the filter and the map chained together:

  wordStream.filter(word -> word.length() > 5).map(word -> word.toUpperCase());

These expressions can start to get rather long (and hard to read) when chained together (even in these notes they wrap around the line), so it is better to put each call to a stream operator on its own line, as follows:

  wordStream.filter(word -> word.length() > 5)

            .map(word -> word.toUpperCase());

(note that here you do NOT put a semicolon at the end of the line with filter because the map on the next line is part of the same computation. Only use a semi-colon at the end of a computation.)

3 Converting Between Streams and Lists

You will often continue to work with Lists in your own code, switching to streams to perform filter, map, and similar operations, then converting back to Lists when you return to your own code. You therefore need to know how to convert between streams and lists.

Converting from a list to a stream is straightforward. The List classes have a method called stream() that converts the list into a stream. For example:

  LinkedList<String> words = ...

  Stream<String> wordStream = words.stream();

Converting from a stream back to a list is a bit messier, because there are actually many different kinds of lists in Java (the differences between them are beyond the scope of this course). We have to tell Java to create a LinkedList in particular. Here’s an example of how to do that:

  LinkedList<String> longWords =

      words.stream().filter(word -> word.length() > 5)

           .collect(Collectors.toCollection(LinkedList::new));

4 Other Operations on Streams

The Java stream library provides several other operations on streams. One of the more useful is reduce, which aggregates a stream into a single value (as you would need for summing a stream, for example) – this is similar to fold in Racket, if you’ve seen that before (here's some information on using reduce).There are also computation-specific versions of reduce for counting, summing, or-ing, etc.

5 Practice With Streams

5.1 Rainfall

Try writing the Rainfall program (from Lab 3) using streams, maps, and filters. Here's the problem description again:

Design a program called rainfall that consumes a list of real numbers representing daily rainfall readings. The list may contain the number -999 indicating the end of the data of interest. Produce the average of the non-negative values in the list up to the first -999 (if it shows up). There may be negative numbers other than -999 in the list (representing faulty readings). Assume that there is at least one non-negative number before -999.

Here are some interfaces/classes/methods described in the Java API that you might find helpful in solving the Rainfall problem (but note that there are many ways to solve problems using the functions provided by Stream, so don't feel constrained to using these):

If you finish the Rainfall problem, you may try some of these other problems from our previous lecture on Planning:

5.2 Adding Machine

Design a program called adding-machine that consumes a list of numbers and produces a list of the sums of each non-empty sublist separated by zeros. Ignore input elements that occur after the first occurrence of two consecutive zeros.

Example:

  adding-machine([list: 1, 2, 0, 7, 0, 5, 4, 1, 0, 0, 6])

    is [list: 3, 7, 10]

5.3 Shopping Cart

An online clothing store applies discounts during checkout. A shopping cart is a list of the items being purchased. Each item has a name (a string like “shoes”) and a price (a real number like 12.50). Design a program called checkout that consumes a shopping cart and produces the total cost of the cart after applying the following two discounts:

Use the following classes for items and carts:

  class CartItem {

    String name;

    double price;

  

    CartItem (String name, double price) {

      this.name = name;

      this.price = price;

    }

  }

  

  // A sample cart in your Examples class

  LinkedList<CartItem> cart;

5.4 Sum Over Table

Assume that we represent tables of numbers as lists of rows, where each row is itself a list of numbers. The rows may have different lengths. Design a program sumLargest that consumes a table of numbers and produces the sum of the largest item from each row. Assume that no row is empty.

Example:

  sum-largest(

      [list: [list: 1, 7, 5, 3], [list: 20], [list: 6, 9]])

    is 36 (from 7 + 20 + 9)

6 What’s So Great About Lambda Expressions?

Lambda expressions are far more verbose in Java than in Racket, so why would you want to use them in Java? In a nutshell, implementations of streams are free to operate on the stream elements in parallel. If you write a for-loop over a list, in constrast, you force Java to handle each element one at a time. In case you are interested, this article summarizes what goes on under the hood of the streams library and why it matters.

7 What to Turn In

As usual, submit your code files in InstructAssist. The name of the project in InstructAssist is Lab 6.