Java 8 Features
Java 8 (March 2014) is the most consequential release in the language’s history. It introduced functional programming primitives — lambdas, method references, and the Stream API — alongside a redesigned date/time library and interface default methods. Code written before and after Java 8 reads like two different languages. Everything here is still idiomatic on modern JDKs, so this is the foundation for the rest of the Advanced track.
Lambdas and Functional Interfaces
A lambda is an anonymous implementation of a functional interface — an interface with exactly one abstract method (SAM). The compiler infers the target type from context, so the syntax stays terse.
Runnable r = () -> System.out.println("running");
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());
The java.util.function package ships the canonical functional interfaces, so you rarely declare your own.
| Interface | Signature | Typical use |
|---|---|---|
Function<T,R> | R apply(T) | transform a value |
Predicate<T> | boolean test(T) | filter / match |
Consumer<T> | void accept(T) | side effect |
Supplier<T> | T get() | lazy produce |
BiFunction<T,U,R> | R apply(T,U) | two-arg transform |
Annotate your own SAM interfaces with @FunctionalInterface so the compiler enforces the single-method contract.
Method References
When a lambda merely forwards its arguments to an existing method, a method reference is clearer.
list.forEach(System.out::println); // instance method of an arbitrary object
names.stream().map(String::toUpperCase); // unbound instance method
Supplier<List<String>> f = ArrayList::new; // constructor reference
The four forms are Type::staticMethod, instance::method, Type::instanceMethod, and Type::new.
The Stream API
Streams express data processing as a declarative pipeline: a source, zero or more lazy intermediate operations, and one eager terminal operation. They don’t store data and don’t mutate the source.
import java.util.*;
import java.util.stream.*;
List<String> names = List.of("Ada", "Linus", "Grace", "Alan");
Map<Integer, List<String>> byLength = names.stream()
.filter(n -> n.length() > 3)
.sorted()
.collect(Collectors.groupingBy(String::length));
System.out.println(byLength);
Output:
{4=[Alan], 5=[Grace, Linus]}
Intermediate operations are lazy — nothing executes until a terminal operation (
collect,forEach,reduce,count) pulls elements through. A pipeline with no terminal operation does no work.
For aggregation over numbers, prefer the primitive streams (IntStream, LongStream, DoubleStream) to avoid boxing:
int total = IntStream.rangeClosed(1, 100).sum(); // 5050
Optional
Optional<T> is a container that explicitly models “value or absence,” replacing null returns and the NullPointerExceptions they invite.
Optional<User> found = repo.findById(id);
String name = found.map(User::name).orElse("unknown");
Use Optional as a return type for “might be empty” results. Do not use it for fields, parameters, or collections — an empty collection already models absence.
The java.time API
The old java.util.Date/Calendar were mutable and error-prone. java.time (JSR-310) is immutable, thread-safe, and clearly separates concepts.
import java.time.*;
LocalDate today = LocalDate.of(2026, 6, 1);
LocalDate due = today.plusWeeks(2);
Period left = Period.between(today, due);
ZonedDateTime meeting = ZonedDateTime.of(
LocalDateTime.of(2026, 6, 15, 9, 30), ZoneId.of("America/New_York"));
| Type | Represents |
|---|---|
LocalDate | date, no time/zone |
LocalTime | time, no date |
LocalDateTime | date + time, no zone |
ZonedDateTime | date + time + zone |
Instant | machine timestamp (UTC) |
Duration / Period | time-based / date-based amounts |
Default Methods
Interfaces can now ship method bodies via default, letting libraries add methods without breaking existing implementations — this is exactly how Collection.stream() and Iterable.forEach() were retrofitted.
interface Greeter {
String name();
default String greet() { return "Hello, " + name(); }
}
Best Practices
- Reach for streams when they clarify intent; a plain
forloop is fine for trivial iteration or when you need to mutate external state. - Keep lambdas short and side-effect-free. Extract a named method (and use a method reference) once logic grows past a line or two.
- Never call
Optional.get()without first checkingisPresent()— preferorElse,orElseThrow, ormap. - Always use
java.timefor new code; treatDate/Calendaras legacy interop only. - Store and pass
Instant/ZonedDateTimefor timestamps; convert toLocalDateTimeonly at the display boundary.