Java Lambdas and Streams

Write short functions with lambdas and process collections with streams: filter, map, sorted, reduce and collect.

Lesson 79 of 127Advanced18 min read

Much of everyday programming is processing collections: keep only the products in stock, turn names into upper case, add up the prices, find the top three scores, group orders by customer. With loops, each of those takes several lines and a temporary variable or two. Since Java 8, there's a more expressive way: lambdas and streams.

A lambda is a short, unnamed function you can pass around like a value: x -> x * 2. A stream is a pipeline that takes items from a collection and passes them through a series of steps, such as filter, map and sorted, before collecting the result. The code reads almost like a description of what you want: "from the products, keep the ones in stock, take their names, sort them, collect them into a list".

Streams are used throughout modern Java, from Android apps to large Spring services. In this lesson you'll write lambdas and method references, build stream pipelines, summarise data with reduce and collectors, and group items with groupingBy.

Lambda expressions

A lambda has parameters, an arrow and a body:

Java
x -> x * 2                       // one parameter
(a, b) -> a + b                  // two parameters
name -> name.toUpperCase()       // calls a method
(x) -> { int y = x * 2; return y + 1; }   // a block body

Lambdas can be used wherever Java expects a functional interface: an interface with exactly one abstract method, such as Comparator, Runnable or the ones in java.util.function, like Predicate<T> (returns a boolean), Function<T, R> (turns a T into an R) and Consumer<T> (does something with a T).

Method references

When a lambda just calls an existing method, a method reference is shorter:

LambdaMethod reference
s -> s.toUpperCase()String::toUpperCase
x -> System.out.println(x)System.out::println
s -> Integer.parseInt(s)Integer::parseInt
p -> p.price()Product::price

Use whichever is clearer. Method references shine in simple cases, while lambdas are better when you need a small calculation.

Building a stream pipeline

A pipeline has three parts:

  1. A source: list.stream(), Arrays.stream(array) or Stream.of(a, b, c).
  2. Intermediate operations, which return a new stream: filter(predicate), map(function), sorted(), distinct(), limit(n).
  3. A terminal operation, which produces the result: collect(...), toList(), count(), forEach(...), reduce(...), anyMatch(...).

Nothing happens until the terminal operation runs; streams are lazy. A stream can also be used only once, and it never changes the original collection. toList() (Java 16+) returns an unmodifiable list; use collect(Collectors.toList()) on older versions.

Summarising and grouping

For numbers, mapToInt or mapToDouble gives a numeric stream with handy terminal operations: sum(), average(), max() and summaryStatistics(). reduce combines all items into one value: stream.reduce(0, Integer::sum).

The Collectors class builds more complex results:

Java
Map<String, List<Product>> byCategory =
    products.stream().collect(Collectors.groupingBy(Product::category));

groupingBy with a second collector, such as Collectors.counting() or Collectors.summingDouble(...), gives a count or total per group in one line. Collectors.joining(", ") joins strings.

Optional results and short-circuiting

Some terminal operations might not find anything. findFirst(), max() and min() therefore return an Optional, a box that is either empty or holds one value:

Java
String longWord = words.stream()
    .filter(w -> w.length() > 5)
    .findFirst()
    .orElse("none");

orElse supplies a default when the Optional is empty, and isPresent(), ifPresent() and orElseThrow() give other ways to handle the missing case. The Optional lesson covers it fully.

Operations such as findFirst, anyMatch and limit are short-circuiting: the stream stops pulling items as soon as the answer is known. Because streams are lazy, a pipeline that filters a million items and then calls findFirst() may only look at the first few.

Streams or loops?

Streams are best for transforming and summarising data: filter, map, group, count. A plain loop is often clearer when the work involves several variables changing together, early returns with complicated conditions, or checked exceptions, which lambdas can't throw without extra code. Both are fine Java; choose the one a colleague will understand fastest. The stream API and collectors lesson has more advanced pipelines.

Java Streams Syntax

Stream syntax

list.stream()
    .filter(x -> condition)
    .map(x -> transform)
    .sorted()
    .limit(n)
    .toList();

list.stream().mapToDouble(Item::price).sum();
list.stream().collect(Collectors.groupingBy(Item::category, Collectors.counting()));

Java Streams Summary

  • A lambda is a short function: (params) -> expression.
  • Lambdas implement functional interfaces such as Predicate, Function and Comparator.
  • Method references like String::length are shorthand for simple lambdas.
  • A stream has a source, intermediate operations (filter, map, sorted) and one terminal operation.
  • Collectors.groupingBy, counting, joining and numeric streams summarise data.

Java Streams Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Filtering and transforming names

Java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = List.of("ana", "Ben", "cleo", "dev", "Ema", "Finn");

        List<String> result = names.stream()
            .filter(n -> n.length() <= 3)
            .map(n -> n.substring(0, 1).toUpperCase() + n.substring(1).toLowerCase())
            .sorted()
            .toList();

        System.out.println(result);
        System.out.println("Starting with E or F: " +
            names.stream().filter(n -> "EF".contains(n.substring(0, 1).toUpperCase())).count());
    }
}
Output
[Ana, Ben, Dev, Ema]
Starting with E or F: 2
Line-by-line explanation
  1. Line 7filter keeps only names of three letters or fewer.
  2. Line 8map capitalises each name properly.
  3. Lines 9 to 10sort alphabetically and collect the result into a list.
  4. Line 14count() is a terminal operation that returns how many items passed the filter.
Example 2

Totals and averages from objects

Java
import java.util.List;

public class Main {
    record Order(String customer, int qty, double price) {
        double total() { return qty * price; }
    }

    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("Ana", 1, 34.99), new Order("Ben", 5, 3.50),
            new Order("Ana", 10, 1.20), new Order("Dev", 1, 24.50));

        double revenue = orders.stream().mapToDouble(Order::total).sum();
        double biggest = orders.stream().mapToDouble(Order::total).max().orElse(0);
        boolean anyLarge = orders.stream().anyMatch(o -> o.qty() >= 10);

        System.out.printf("Revenue %.2f, biggest order %.2f, bulk order? %b%n",
            revenue, biggest, anyLarge);
    }
}
Output
Revenue 88.99, biggest order 34.99, bulk order? true
Line-by-line explanation
  1. Lines 4 to 6A record with a small method that calculates the order's value.
  2. Line 13mapToDouble turns each order into its total, then sum() adds them up.
  3. Line 14max() returns an OptionalDouble, because the stream might be empty; orElse supplies a fallback.
  4. Line 15anyMatch stops as soon as one order matches.
Example 3

Grouping and counting

Java
import java.util.*;
import java.util.stream.Collectors;

public class Main {
    record Product(String name, String category, double price) {}

    public static void main(String[] args) {
        List<Product> products = List.of(
            new Product("Notebook", "Stationery", 3.50),
            new Product("Pen", "Stationery", 1.20),
            new Product("Water bottle", "Kitchen", 12.00),
            new Product("Lunch box", "Kitchen", 8.75),
            new Product("Desk lamp", "Home", 24.50));

        Map<String, Long> counts = products.stream()
            .collect(Collectors.groupingBy(Product::category, TreeMap::new, Collectors.counting()));
        System.out.println(counts);

        String cheap = products.stream()
            .filter(p -> p.price() < 10)
            .map(Product::name)
            .collect(Collectors.joining(", "));
        System.out.println("Under 10: " + cheap);
    }
}
Output
{Home=1, Kitchen=2, Stationery=2}
Under 10: Notebook, Pen, Lunch box
Line-by-line explanation
  1. Lines 15 to 16groupingBy splits the products by category and counting() counts each group. TreeMap::new keeps the categories sorted.
  2. Lines 19 to 22Keep cheap products, take their names and join them into one string.

Common Java Streams Mistakes

Reusing a stream

Don't
Stream<String> s = names.stream();
long n = s.count();
List<String> l = s.toList();   // IllegalStateException: stream has already been operated upon
Do
long n = names.stream().count();
List<String> l = names.stream().toList();

Forgetting the terminal operation

Don't
names.stream().filter(n -> n.startsWith("A"));   // nothing happens
Do
List<String> aNames = names.stream().filter(n -> n.startsWith("A")).toList();

Streams are lazy: without a terminal operation, the pipeline never runs.

Changing outside variables inside a stream

Don't
List<String> upper = new ArrayList<>();
names.stream().forEach(n -> upper.add(n.toUpperCase()));
Do
List<String> upper = names.stream().map(String::toUpperCase).toList();

Java Streams in Real Projects

Streams in modern Java code

Business applications use streams to filter and summarise data loaded from databases, such as revenue per region or the ten most active customers. Spring and Android code is full of lambdas for event handlers and callbacks, and groupingBy replaces what used to be a dozen lines of map-building code.

Java Streams FAQ

Are Java streams faster than loops?

Not usually. For simple work on small and medium collections, a loop is often slightly faster, because a stream has some setup cost. The difference rarely matters, so choose streams for readability. Measure before optimising, and don't expect parallelStream() to help unless the work per item is large.

What is the difference between map and flatMap?

map turns each item into exactly one new item. flatMap turns each item into a stream of zero or more items and joins those streams together. For example, flatMap(List::stream) turns a list of lists, [[1, 2], [3]], into the single stream 1, 2, 3.

Why can't a lambda change a local variable?

Local variables used inside a lambda must be final or effectively final, meaning they are never reassigned. Otherwise the compiler reports "local variables referenced from a lambda expression must be final or effectively final". Let the stream produce the result instead, with sum(), count() or collect().

What is the difference between toList() and collect(Collectors.toList())?

toList(), added in Java 16, returns a list that can't be changed. collect(Collectors.toList()) currently returns an ArrayList, but the documentation makes no promise about whether it can be changed. Use collect(Collectors.toCollection(ArrayList::new)) when you specifically need a list you can modify.

How do I debug a stream pipeline?

Split the pipeline into steps stored in variables, or temporarily add .peek(x -> System.out.println(x)) between operations to see each item pass through. Most IDEs, including IntelliJ IDEA, also have a stream debugger that shows the items at every stage.

Java Streams Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the lambda so it doubles a number.

Function<Integer, Integer> doubler = x  x * 2;
Predict the outputeasy

What does this print?

System.out.println(List.of(1, 2, 3, 4, 5).stream().filter(n -> n % 2 == 1).toList());
Predict the outputmedium

What does this print?

int total = List.of(3, 4, 5).stream().map(n -> n * n).reduce(0, Integer::sum);
System.out.println(total);
Multiple choicemedium

Which is a terminal operation?

Multiple choicehard

What is a functional interface?

Java Streams Quiz

5 questions to check what you learned.

  1. What does filter do in a stream?

    • Changes every item
    • Keeps only the items that match a condition
    • Sorts the items
    • Counts the items
  2. What does map do in a stream?

    • Transforms each item into a new value
    • Creates a HashMap
    • Removes duplicates
    • Groups items
  3. What is String::toUpperCase?

    • A variable
    • A static import
    • A comment
    • A method reference, shorthand for s -> s.toUpperCase()
  4. Why do streams need a terminal operation?

    • For error handling
    • To close the collection
    • Streams are lazy; the pipeline only runs when a terminal operation asks for a result
    • To sort the results
  5. What does Collectors.groupingBy(Product::category, Collectors.counting()) produce?

    • A list of categories
    • A map from each category to the number of products in it
    • The total price per category
    • A sorted list of products