Skip to content
Java advanced 3 min read

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.

InterfaceSignatureTypical 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"));
TypeRepresents
LocalDatedate, no time/zone
LocalTimetime, no date
LocalDateTimedate + time, no zone
ZonedDateTimedate + time + zone
Instantmachine timestamp (UTC)
Duration / Periodtime-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 for loop 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 checking isPresent() — prefer orElse, orElseThrow, or map.
  • Always use java.time for new code; treat Date/Calendar as legacy interop only.
  • Store and pass Instant/ZonedDateTime for timestamps; convert to LocalDateTime only at the display boundary.
Last updated June 1, 2026
Was this helpful?