1.4-Software-Development-Principles

Streams part1

Learning goals

Requirements: To understand this concept you have to be comfortable with the idea of interfaces in Java. We also expect a basic understanding of lambda expressions.

For the examples in this theory section we’re using the code from the IndianDishQuiz assignment. Specifically an IndianDish.

The data can be found in the indian_food.csv.

Note: In this diagram the ingredients are split into a list of strings as well. (Instead of just one comma separated string.)

An example

After reading the theory you should be able to recognise and design the following code segment:

// Using a list of integers.
List<Integer> list = Arrays.asList(8, 4, 5, 7, 2);

int totalTimes2 =
    // Take the list.
    list
        // Use it as a stream.
        .stream()
        // Convert (map) each integer using this calculation (lambda expression)
        .map(x -> x * 2)
        // Add the resulting integers together (another lambda expression)
        .reduce(Integer::sum);

// Answer should be (8*2 + 4*2 + 5*2 + 7*2 + 2*2 =) 52
System.out.println(totalTimes2);

What are streams?

After a year of programming you may have noticed that most software relies heavily on storing and processing collections of data. Everything that we’re about to show you can be done using the Java knowledge that you already have.

There is one problem: Although YOU know how to do this in Java, expressing the corresponding ideas behind your code can be difficult. Other colleagues may communicate much more easily using database terms.

For instance: “From the list of dishes I want to see only the ones that are vegetarian.” SELECT * FROM dishes WHERE diet='vegetarian' This simple notion takes Java, using only collections, quite some steps to figure out:

ArrayList<Dish> result = new ArrayList<Dish>();
for (Dish dish:dishes) {
  if (dish.getDiet().equals("vegetarian")) {
    result.add(dish);
  }
}
return result;

Streams are an update to Java that represent lists or collections as a sequence of elements. These streams also provide a standard set of operations to filter, sort or update each of those elements. Previously each developer had to write their own methods to do this.

Note: As said before, you can still do so. But once you understand streams your code looks much more like a query, and can be easily adapted should that query require an update.

Another example

Using streams we want to show then names of dishes with less than 400 calories ordered by their calories:

// We're using a list of menu items.
List<MenuItem> menu = readMenu();
List<String> lowCaloricDishesName =
    // Using the menu collection.
    menu
        // Start by providing the stream source.
        .stream()
        // Only select dishes WHERE calories < 400
        .filter(d -> d.getCalories() < 400)
        // Order using those calories
        .sorted(comparing(Dish::getCalories))
        // Select the name of the dish.
        .map(Dish::getName)
        // Add the result to a list again.
        .collect(toList());               

In this code .filter, .sort, .map and .collect are standard operations on a stream that allow you to define the steps that manipulate the original content and result in the answer you want.

The code contains three parts:

The rest of the theory will show you the most common operations on a stream.

One side node: In this code the intermediate operations are chained together: stream.doSomething().doAnotherThing().etc() This is called creating a pipeline of operations. Each operation gives you another stream that contains slightly different data along the way.

This may cause very long lines of code, so we split them on the dots.

Common stream operations

Creating a stream (source)

There are several ways to create a stream, the first two are the most common ones.

Stream<Integer> stream = Stream.of(1,2,3,4,5);
Stream<Integer> stream = Stream.iterate(0, n->n+1).limit(10);

(Take your time to wrap your head around that last one: Create a sequence starting at 0. Increase each next value. (In this case by 1) Please stop after 10 elements.)

Intermediate operations

After calling these methods you are presented with a new (view on) the stream that you can further manipulate.

.filter(x -> x > 5)

.filter(x -> x.equals(">")

.map(x -> x - 1)

.map(dish -> dish->getCalories())

.limit(10)

.sorted(comparing(Dish::getCalories))

.distinct()

Terminal operations

These methods take a stream and convert to a single result. (This result is not a stream otherwise it would have been an intermediate method.)

.count()

.reduce(Integer::sum)

.collect(Collectors.toList())

Primitive Streams

Streams as defined so far seem to require reference types as their elements. For instance Stream<String> or Stream<Integer>.

To use with primitive types there are specialised implementations:

To convert any stream to a specific primitive stream a special conversion is required:

Aggregation functions:

This allows dedicated terminal operation:

These are the basic aggregation function we know (and love) from databases except for .count() because that one doesn’t require a primitive type stream.

Collection<Dish> yourOrder = getOrder();

int totalCalories = yourOrder
    .stream()
    .mapToInt(dish -> dish.getCalories())
    .sum();
    
System.out.println("Total calories in your order : "+totalCalories);

Advanced uses of Streams

As stated before the Dish class itself contains a list of ingredients used in that dish. Let’s say that we have an order for a certain set of dishes. How would you get a list of all the required ingredients.

Effectively this is an array within an array: (Please forgive the JSON notation.)

[
    { 
        "name" : "Dish 1",
        "ingredients" : [ "apples", "cumin", "rice" ]
    },
    {
        "name" : "Dish 2",
        "ingredients" : [ "rice", "sugar", "ghee", "cinnamon" ]
    },
    {
        // etc.
    }
]

I would like to get a list that contains only:

["apples", "cumin", "rice", "sugar", "ghee", "cinnamon"]

You can solve this problem using the .flatMap intermediate operation:

List<Dish> order = getOrderList();
List<String> uniqueIngredients =
    order
        .stream()
        // Temporarily create a stream that only contains arraylists.
        .map(d -> d.getIngredients())
        // Merge those separate arraylists into a single stream.
        .flatMap(ArrayList::stream)
        // Filter out duplicates.
        .distinct()
        .collect(Collectors.toList());