Week 3

Collections

Generics

Enums

Stream APIs

Unit Testing with JUnit

Debugging in Java

Practice

Assignment

Back end Track

Under construction

<aside> 🚧

This page is currently under construction. Please check back later.

</aside>

Introduction

Do you miss functional programming? Maybe just the clean, chainable elegance of JavaScript array methods like .filter(), .map() and .reduce()? Good news: Java's Stream API (introduced in Java 8) brings a very similar functional programming flavor right into the object-oriented world of Java.

With Stream API, you can have flavors of functional programming within object-oriented programming. This hybrid approach means you can combine the advantages of both to effortlessly achieve your application goals!

In this chapter, we'll cover lazy processing—the concept behind the Stream API—and how to create streams. We'll explore intermediate operations like .filter(), .map(), and .sorted(), then move on to terminal operations like .collect(), .forEach(), and .count(). You'll learn how intermediate and terminal operations relate to lazy processing, and we'll work through interactive examples along the way!

Declarative Data Processing

The Java Stream API (introduced in Java 8) lets you process collections in a declarative style rather than the traditional imperative style. Let’s elaborate the difference between the 2:

Imperative code

You describe how to perform an operation step-by-step. Much like commands, do this, then that etc…

In the below code we have a list of strings as grocery items but we want to transform that into a richer data with an extra property. Then the imperative way would be to carry the transform out step by step like so:

// Class we want to transform to
public class GroceryItem  {
    // (String name, int quantity)
    private String name;
    private Integer quantity;

    public  GroceryItem(String name, Integer quantity) {
        this.name = name;
        this.quantity = quantity;
    }

    public String toString() {
        return this.name + " | Qty: " + this.quantity;
    }
}

void main() {
    var quantityRand = new Random();
    List<String> groceries = new ArrayList<>();

    groceries.add("Milk");
    groceries.add("Eggs");
    groceries.add("Banana");

    List<GroceryItem> transformedGroceries = new ArrayList<>();
    for (String item : groceries) {
        int quantity = quantityRand.nextInt(0, 10);
        GroceryItem groceryItem = new GroceryItem(item, quantity);
        transformedGroceries.add(groceryItem);
    }

    IO.println(transformedGroceries);
}

In imperative code, you describe how to perform an operation step-by-step using loops, conditionals, and mutable variables. In declarative code, you describe what result you want — the "how" is handled by the Stream library.

Implementation plan:

  1. What is the Stream API — declarative data processing
  2. Creating a stream from a collection: .stream()
  3. Intermediate operations: .filter(), .map(), .sorted()
  4. Terminal operations: .collect(), .forEach(), .count()
  5. Collecting to a list: Collectors.toList()
  6. Chaining operations — building a pipeline
  7. Streams are lazy — intermediate ops don't execute until terminal op
  8. Streams vs loops — when to use which
  9. Exercise: transform and filter a list of objects using streams

The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0

CC BY-NC-SA 4.0 Icons

*https://hackyourfuture.net/*

Found a mistake or have a suggestion? Let us know in the feedback form.