Stream API & Optional
The Stream API (Java 8+)
A Stream<T> is a sequence of elements that supports functional-style operations. Streams do not store data — they compute values on demand from a source (collection, array, generator).
Creating Streams
collection.stream()— sequential streamcollection.parallelStream()— parallel streamStream.of(a, b, c)— stream of known valuesArrays.stream(arr)— stream from arrayStream.generate(supplier)/Stream.iterate(seed, fn)— infinite streams
Intermediate Operations (lazy — produce another stream)
filter(Predicate)— keep matching elementsmap(Function)— transform each elementflatMap(Function<T, Stream<R>>)— flatten nested streamsdistinct()— remove duplicatessorted()/sorted(Comparator)— sortlimit(n)/skip(n)— slice the streampeek(Consumer)— inspect elements without consuming (useful for debugging)
Terminal Operations (eager — trigger computation)
collect(Collector)— accumulate into a collection or other containerforEach(Consumer)— iteratecount()— number of elementsreduce(identity, BinaryOperator)— foldmin(Comparator)/max(Comparator)— returnOptional<T>findFirst()/findAny()— returnOptional<T>anyMatch / allMatch / noneMatch(Predicate)— boolean short-circuits
Collectors
Collectors.toList()/toSet()/toUnmodifiableList()Collectors.joining(delimiter)— concatenate stringsCollectors.groupingBy(classifier)—Map<K, List<V>>Collectors.counting(),summingInt(),averagingInt()
Optional<T> (Java 8+)
Optional<T> is a container that either holds a non-null value or is empty. It is designed to be a return type — a method returning Optional<User> explicitly communicates that the result may be absent.
Key methods:
Optional.of(value)— must not be nullOptional.ofNullable(value)— null becomes emptyOptional.empty()— explicitly emptyisPresent()/isEmpty()(Java 11)get()— throws if empty (avoid if possible)orElse(default)/orElseGet(Supplier)/orElseThrow()map(Function)/flatMap(Function)/filter(Predicate)— transform the contained value
Code Examples
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamPipelineDemo {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Marketing", 72000),
new Employee("Carol", "Engineering", 110000),
new Employee("Dave", "Marketing", 68000),
new Employee("Eve", "Engineering", 88000),
new Employee("Frank", "HR", 61000)
);
// 1. Filter + map + collect to list
List<String> highEarners = employees.stream()
.filter(e -> e.salary() > 85000)
.map(Employee::name)
.sorted()
.collect(Collectors.toList());
System.out.println("High earners: " + highEarners);
// 2. Count matching elements
long engCount = employees.stream()
.filter(e -> "Engineering".equals(e.dept()))
.count();
System.out.println("Engineers : " + engCount);
// 3. Reduce — total salary
double total = employees.stream()
.mapToDouble(Employee::salary)
.sum();
System.out.printf("Total salary: $%.0f%n", total);
// 4. Joining — comma-separated names
String names = employees.stream()
.map(Employee::name)
.collect(Collectors.joining(", "));
System.out.println("Names : " + names);
// 5. flatMap — flatten list of lists
List<List<Integer>> nested = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5),
Arrays.asList(6, 7, 8, 9)
);
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
System.out.println("Flat : " + flat);
}
}Stream pipelines are lazy: intermediate operations (filter, map) are only executed when a terminal operation (collect, count, sum) is invoked. mapToDouble returns a DoubleStream with numeric aggregation methods.
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingByDemo {
record Product(String name, String category, double price) {}
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("Laptop", "Electronics", 1200.0),
new Product("Phone", "Electronics", 800.0),
new Product("Desk", "Furniture", 350.0),
new Product("Chair", "Furniture", 200.0),
new Product("Headphones","Electronics", 150.0),
new Product("Bookshelf", "Furniture", 120.0)
);
// Group by category -> List<Product>
Map<String, List<Product>> byCategory = products.stream()
.collect(Collectors.groupingBy(Product::category));
byCategory.forEach((cat, list) -> {
List<String> names = list.stream().map(Product::name).collect(Collectors.toList());
System.out.println(cat + ": " + names);
});
// Count per category
Map<String, Long> countByCategory = products.stream()
.collect(Collectors.groupingBy(Product::category, Collectors.counting()));
System.out.println("Counts: " + countByCategory);
// Average price per category
Map<String, Double> avgPrice = products.stream()
.collect(Collectors.groupingBy(Product::category,
Collectors.averagingDouble(Product::price)));
avgPrice.forEach((cat, avg) ->
System.out.printf("Avg %s: $%.2f%n", cat, avg));
// Summary statistics for all prices
DoubleSummaryStatistics stats = products.stream()
.collect(Collectors.summarizingDouble(Product::price));
System.out.printf("Min: $%.0f Max: $%.0f Avg: $%.2f%n",
stats.getMin(), stats.getMax(), stats.getAverage());
}
}groupingBy is a downstream collector that partitions elements into a Map. The second argument to groupingBy specifies how to aggregate each group — counting(), averagingDouble(), or any other collector.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class OptionalDemo {
record User(String name, String email) {}
static Optional<User> findUser(List<User> users, String name) {
return users.stream()
.filter(u -> u.name().equals(name))
.findFirst();
}
static Optional<String> getEmailDomain(String email) {
if (email == null || !email.contains("@")) return Optional.empty();
return Optional.of(email.split("@")[1]);
}
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("Alice", "alice@example.com"),
new User("Bob", null),
new User("Carol", "carol@company.org")
);
// orElse — provide a default
String name = findUser(users, "Dave")
.map(User::name)
.orElse("Unknown");
System.out.println("Found : " + name);
// map + orElse — transform if present
String email = findUser(users, "Alice")
.map(User::email)
.orElse("no email");
System.out.println("Email : " + email);
// flatMap — chaining Optional-returning methods
String domain = findUser(users, "Carol")
.map(User::email)
.flatMap(OptionalDemo::getEmailDomain)
.orElse("unknown domain");
System.out.println("Domain : " + domain);
// Bob has null email — ofNullable prevents NPE
String bobDomain = findUser(users, "Bob")
.flatMap(u -> Optional.ofNullable(u.email()))
.flatMap(OptionalDemo::getEmailDomain)
.orElse("no domain");
System.out.println("Bob domain: " + bobDomain);
// filter — only keep if condition met
Optional<User> adminUser = findUser(users, "Alice")
.filter(u -> u.email() != null && u.email().endsWith(".com"));
System.out.println("Admin : " + adminUser.map(User::name).orElse("none"));
// orElseThrow — throw if absent
try {
findUser(users, "Ghost").orElseThrow(
() -> new java.util.NoSuchElementException("User not found"));
} catch (java.util.NoSuchElementException e) {
System.out.println("Exception : " + e.getMessage());
}
}
}Optional chains prevent null checks from cluttering the call site. flatMap is used when a transformation itself returns an Optional, avoiding Optional<Optional<T>>. Never use get() without isPresent() — prefer orElse, orElseGet, or orElseThrow.
Quick Quiz
1. When are intermediate stream operations (filter, map) actually executed?
2. Which Collectors method groups stream elements into a `Map<K, List<V>>`?
3. What is the difference between `Optional.of(value)` and `Optional.ofNullable(value)`?
4. Which operation would you use to transform a `Stream<List<String>>` into a flat `Stream<String>`?
Was this lesson helpful?