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:
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 bodyLambdas 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:
| Lambda | Method 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:
- A source:
list.stream(),Arrays.stream(array)orStream.of(a, b, c). - Intermediate operations, which return a new stream:
filter(predicate),map(function),sorted(),distinct(),limit(n). - 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:
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:
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::lengthare shorthand for simple lambdas. - A stream has a source, intermediate operations (filter, map, sorted) and one terminal operation.
Collectors.groupingBy,counting,joiningand numeric streams summarise data.
Java Streams Examples
Each example shows the code, the output it produces and what each part does.
Filtering and transforming names
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());
}
}[Ana, Ben, Dev, Ema]
Starting with E or F: 2Line-by-line explanation
- Line 7filter keeps only names of three letters or fewer.
- Line 8map capitalises each name properly.
- Lines 9 to 10sort alphabetically and collect the result into a list.
- Line 14count() is a terminal operation that returns how many items passed the filter.
Totals and averages from objects
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);
}
}Revenue 88.99, biggest order 34.99, bulk order? trueLine-by-line explanation
- Lines 4 to 6A record with a small method that calculates the order's value.
- Line 13mapToDouble turns each order into its total, then sum() adds them up.
- Line 14max() returns an OptionalDouble, because the stream might be empty; orElse supplies a fallback.
- Line 15anyMatch stops as soon as one order matches.
Grouping and counting
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);
}
}{Home=1, Kitchen=2, Stationery=2}
Under 10: Notebook, Pen, Lunch boxLine-by-line explanation
- Lines 15 to 16groupingBy splits the products by category and counting() counts each group. TreeMap::new keeps the categories sorted.
- Lines 19 to 22Keep cheap products, take their names and join them into one string.
Common Java Streams Mistakes
Reusing a stream
Stream<String> s = names.stream();
long n = s.count();
List<String> l = s.toList(); // IllegalStateException: stream has already been operated uponlong n = names.stream().count();
List<String> l = names.stream().toList();Forgetting the terminal operation
names.stream().filter(n -> n.startsWith("A")); // nothing happensList<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
List<String> upper = new ArrayList<>();
names.stream().forEach(n -> upper.add(n.toUpperCase()));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.
Complete the lambda so it doubles a number.
Function<Integer, Integer> doubler = x x * 2;
What does this print?
System.out.println(List.of(1, 2, 3, 4, 5).stream().filter(n -> n % 2 == 1).toList());
What does this print?
int total = List.of(3, 4, 5).stream().map(n -> n * n).reduce(0, Integer::sum);
System.out.println(total);
Which is a terminal operation?
What is a functional interface?
Java Streams Quiz
5 questions to check what you learned.
What does
filterdo in a stream?- Changes every item
- Keeps only the items that match a condition
- Sorts the items
- Counts the items
What does
mapdo in a stream?- Transforms each item into a new value
- Creates a HashMap
- Removes duplicates
- Groups items
What is
String::toUpperCase?- A variable
- A static import
- A comment
- A method reference, shorthand for s -> s.toUpperCase()
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
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