Java 8 — reading fluency.
You will not write deep functional Java in your job. But you will read it on every line of every Spring codebase you ever open. Today's goal isn't mastery — it's fluency. Make this code stop being noise.
Why Java 8 happened
Pre-Java 8, simple operations were astonishingly verbose. Filter a list of users to active ones?
// Pre-Java 8 — yes really
List<User> active = new ArrayList<>();
for (User u : users) {
if (u.isActive()) {
active.add(u);
}
}Five lines for "give me the active users." Other languages had a one-liner. Java 8 (released 2014) added lambdas and the Stream API to fix this:
// Java 8+
List<User> active = users.stream()
.filter(u -> u.isActive())
.collect(Collectors.toList());Three lines. Reads like English. This is what you'll see in every modern codebase.
1 · Lambdas — the syntax
A lambda is a short way to write a method as an expression. The form is:
(parameters) -> bodyThe four shapes
() -> doSomething()— no parametersx -> x * 2— one parameter (parens optional)(a, b) -> a + b— multiple parameters(x) -> { logger.info("got " + x); return x * 2; }— multiline body needs braces + return
That's it. There is no fifth shape.
2 · Functional interfaces — what lambdas attach to
A functional interface is any interface with exactly one abstract method. A lambda implements that method.
// java.util.function.Function — built-in functional interface
public interface Function<T, R> {
R apply(T t);
}
// Lambda implementing Function
Function<String, Integer> length = s -> s.length();
length.apply("hello"); // 5The four functional interfaces you must recognize
| Interface | Method | Use when… |
|---|---|---|
Function<T,R> | R apply(T) | Transform T into R (filter input → output) |
Predicate<T> | boolean test(T) | Test if T satisfies a condition |
Consumer<T> | void accept(T) | Do something with T, return nothing |
Supplier<T> | T get() | Produce a T from nothing |
Function<User, String> getName = u -> u.getName();
Predicate<User> isActive = u -> u.isActive();
Consumer<User> log = u -> System.out.println(u);
Supplier<User> empty = () -> new User();When you see a method signature like List.forEach(Consumer<T> action), you immediately know: "I pass it a lambda that takes a T and returns nothing." No need to look at JavaDoc. Same for Predicate, Function, Supplier.
3 · Method references — even shorter
If your lambda just calls one method, replace it with a method reference using :::
// Lambda
users.forEach(u -> System.out.println(u));
// Method reference — same thing
users.forEach(System.out::println);
// Lambda
users.stream().map(u -> u.getName())
// Method reference
users.stream().map(User::getName)Four kinds:
- Static method:
Integer::parseInt - Instance method on a specific object:
System.out::println - Instance method on a class:
User::getName(the instance comes from the stream) - Constructor:
User::new
4 · Streams — the workhorse
A Stream is a pipeline of operations on a sequence of elements. Three stages:
- Source — where the elements come from (
list.stream(),Stream.of(...)) - Intermediate operations — transformations that return another Stream (
filter,map,sorted,distinct,limit) - Terminal operation — produces a result and ends the stream (
collect,forEach,count,findFirst,reduce)
Intermediate operations don't run until a terminal operation triggers them. You can chain ten .map()s and zero work happens. Then .collect() runs the whole pipeline once. This is why streams can be efficient on large data — they fuse operations.
The operations you'll use 95% of the time
// filter — keep elements matching a predicate
users.stream().filter(u -> u.isActive())
// map — transform each element
users.stream().map(User::getEmail)
// sorted — order them
users.stream().sorted(Comparator.comparing(User::getName))
// distinct — remove duplicates
emails.stream().distinct()
// limit — take first N
users.stream().limit(10)
// count — how many?
long n = users.stream().filter(User::isActive).count();
// collect — turn into List/Set/Map
List<String> emails = users.stream()
.filter(User::isActive)
.map(User::getEmail)
.collect(Collectors.toList());Collectors — turning a stream into something concrete
// To List
.collect(Collectors.toList())
// To Set (deduplicates)
.collect(Collectors.toSet())
// To Map — ID → User
.collect(Collectors.toMap(User::getId, u -> u))
// Group by department
.collect(Collectors.groupingBy(User::getDepartment))
// Java 16+ shorthand
.toList() // returns immutable listThe full pipeline — read this fluently
// Get the email addresses of the 5 most recent active premium users
List<String> emails = users.stream()
.filter(User::isActive)
.filter(u -> u.getTier() == Tier.PREMIUM)
.sorted(Comparator.comparing(User::getCreatedAt).reversed())
.limit(5)
.map(User::getEmail)
.toList();Read that aloud. "From users, filter active, filter premium, sort by createdAt descending, take 5, get emails, collect." Once this reads as fluently as English, you've achieved the goal of today.
5 · reduce — fold a stream to one value
// Sum prices
double total = items.stream()
.mapToDouble(Item::getPrice)
.sum();
// Reduce — generic version
int total = numbers.stream().reduce(0, Integer::sum);
// 0 is the identity, Integer::sum is the combine function6 · Optional — modeling absence
Before Java 8, methods returned null to mean "no result." This caused billions of dollars of NullPointerException bugs (the inventor, Tony Hoare, called it "my billion-dollar mistake").
Optional<T> is a wrapper that says: "there might be a T here, or there might not." The compiler can't force you to handle null. It can force you to handle Optional.
// Spring Data JPA returns Optional
Optional<User> maybe = userRepo.findById(id);
// Three good ways to use it
// 1. orElseThrow — fail fast
User user = maybe.orElseThrow(() -> new UserNotFoundException(id));
// 2. orElse — provide a default
User user = maybe.orElse(User.GUEST);
// 3. ifPresent — do something only if present
maybe.ifPresent(u -> emailService.send(u, ...));
// Chain operations with map and filter
String email = maybe
.filter(User::isActive)
.map(User::getEmail)
.orElse("none");- Use it for return types, not parameters or fields
- Never call
.get()directly — useorElseThrow,orElse,ifPresent - Don't return
Optional.of(null)— useOptional.ofNullable(...)for nullable values - Don't write
Optional<List>— return an empty List instead
Quick wins · default methods, var, records
Default methods on interfaces (Java 8)
interface Greeter {
String name();
default String greet() { return "Hello, " + name(); }
}This is how the JDK added Stream to Collection without breaking every implementation. Important to know it exists.
var (Java 10+)
// Compiler infers the type from the right side
var users = userRepo.findAll(); // inferred as List<User>
var map = new HashMap<String, List<User>>(); // no need to repeat the typeRecords (Java 16+)
// One line replaces ~30 lines of getters, equals, hashCode, toString
public record UserDto(String email, String name, int age) { }
// Use it like any class
UserDto dto = new UserDto("a@b.com", "Alice", 30);
String email = dto.email(); // note: email(), not getEmail()If you're targeting interviews in 2026, you should know records exist and be comfortable using them for DTOs. Many new Spring Boot projects use records for request/response payloads. They are immutable, concise, and integrate cleanly with Jackson.
Common mistakes
- Side effects in
map..map(u -> { logger.info(u); return u; })— usepeekor restructure. Streams expect pure functions. - Streams for tiny lists. Don't stream a 3-element list. A
forloop is shorter and clearer. - Reusing a stream. A stream can be terminal-operated once. Build it again from the source.
- Ignoring return values.
list.stream().filter(...)alone does nothing — you need a terminal op. - Nested streams that get unreadable. If your stream is more than 5–6 lines or has deep nesting, refactor to a method.
Lock in today's learning
No coding required. Just answer in words.
- What is a functional interface? Name the four most common ones.
- Read this aloud and explain what it does:
users.stream().filter(User::isActive).map(User::getEmail).toList() - Why are streams lazy, and why does that matter?
- What four ways can you safely extract a value from an
Optional? - What does
::do? Give an example of each of the four kinds. - Why is a
recorda good fit for a Spring Boot DTO?
End of Day 6. Tomorrow: Exceptions — the last piece before we cross into the web era.