Map, Filter, Lambdas and Java Streams
In Friday’s class, you saw how to pass predicates (lambdas) as arguments in Java. This lab shows you how to use this with Java’s versions of map and filter.
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 the predicate shorthand that Professor Hamel showed in class last week. For those who had 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 knowhow 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)); |
You can use ArrayList (if you know those) or other list types in the collector here if you so choose.
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. There are also computation-specific versions of reduce for counting, summing, or-ing, etc.
5 Practice With Streams
Practice writing code with streams, maps, filters, and the other stream operators. You can pick your own problems, or tackle some of the ones we’ve seen in the planning lectures:
5.1 Rainfall
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.
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:
if the cart contains at least 100 worth of shoes, take 20% off the cost of all shoes (match only items whose exact name is "shoes")
if the cart contains at least two hats, take 10 off the total of the cart (match only items whose exact name is "hat")
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?
These are far more verbose than in Racket, so why would people 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.