Why JDK 8?
Released in 2014, JDK 8 brought the most significant paradigm shift in Java’s history. With the introduction of lambda expressions and the Stream API, Java began fully embracing the functional programming style. More than a decade after its release, it remains in use across many production environments today.
This article covers the core features of JDK 8 with runnable examples.
Lambda Expressions and Functional Interfaces
Lambda expressions provide a concise syntax to replace anonymous classes. They allow you to express the implementation of an interface with a single method (a Functional Interface) using just an arrow (->).
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Function;
public class LambdaExample {
public static void main(String[] args) {
List<String> languages = Arrays.asList("Java", "Python", "Go", "Rust", "JavaScript");
// Anonymous class approach (pre-JDK 7)
languages.sort(new java.util.Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
// Lambda expression approach (JDK 8)
languages.sort((a, b) -> a.length() - b.length());
System.out.println("Sorted by length: " + languages);
// Output: Sorted by length: [Go, Java, Rust, Python, JavaScript]
// Predicate: a functional interface for condition checking
Predicate<String> isLong = s -> s.length() >= 5;
// Function: a functional interface for transformation
Function<String, String> toUpper = String::toUpperCase;
// Combining lambdas: convert languages with 5+ characters to uppercase
languages.stream()
.filter(isLong)
.map(toUpper)
.forEach(System.out::println);
// Output:
// PYTHON
// JAVASCRIPT
}
}
The java.util.function package provides pre-defined functional interfaces such as Predicate, Function, Consumer, and Supplier. You can simply combine them without needing to create your own.
Stream API
The Stream API is a pipeline for declaratively processing collection data. Instead of for loops, you chain operations like filter, map, reduce, and collect to write code that clearly expresses its intent.
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter + map + collect: select even numbers and square them
List<Integer> evenSquares = numbers.stream()
.filter(n -> n % 2 == 0) // Filter even numbers
.map(n -> n * n) // Square each value
.collect(Collectors.toList()); // Collect into a list
System.out.println("Squares of evens: " + evenSquares);
// Output: Squares of evens: [4, 16, 36, 64, 100]
// reduce: calculate the total sum
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Sum: " + sum);
// Output: Sum: 55
// groupingBy: group into odd and even
Map<String, List<Integer>> grouped = numbers.stream()
.collect(Collectors.groupingBy(
n -> n % 2 == 0 ? "even" : "odd"
));
System.out.println("Grouped: " + grouped);
// Output: Grouped: {odd=[1, 3, 5, 7, 9], even=[2, 4, 6, 8, 10]}
// Parallel Stream: multi-core parallel processing
long parallelSum = numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
System.out.println("Parallel sum: " + parallelSum);
// Output: Parallel sum: 55
}
}
parallelStream() internally uses the Fork/Join framework to split tasks for parallel execution. It is effective when the dataset is large enough and each element can be processed independently, but for small datasets the overhead may actually make it slower.
Preventing NPE with Optional
NullPointerException has long been the bane of Java developers. Optional expresses “a value may or may not be present” through the type system, preventing missed null checks at compile time.
import java.util.Optional;
public class OptionalExample {
// Method to find a user — the result may not exist
static Optional<String> findUserEmail(String userId) {
if ("admin".equals(userId)) {
return Optional.of("admin@example.com");
}
return Optional.empty(); // Return empty Optional instead of null
}
public static void main(String[] args) {
// orElse: use a default value when empty
String email1 = findUserEmail("admin")
.orElse("unknown@example.com");
System.out.println("Admin email: " + email1);
// Output: Admin email: admin@example.com
String email2 = findUserEmail("guest")
.orElse("unknown@example.com");
System.out.println("Guest email: " + email2);
// Output: Guest email: unknown@example.com
// map + orElse: transform then fall back to default
String domain = findUserEmail("admin")
.map(e -> e.split("@")[1])
.orElse("no domain");
System.out.println("Domain: " + domain);
// Output: Domain: example.com
// ifPresent: execute only when value exists
findUserEmail("admin")
.ifPresent(e -> System.out.println("Recipient: " + e));
// Output: Recipient: admin@example.com
}
}
Optional was originally intended for method return types. Using Optional for fields or method parameters is considered an anti-pattern and should be avoided.
java.time API
The legacy Date and Calendar classes were mutable, not thread-safe, and had unintuitive API designs. The java.time package introduced in JDK 8 solves all of these problems.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
public class DateTimeExample {
public static void main(String[] args) {
// LocalDate: date only (no time)
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(1995, 3, 15);
// Calculate the period between two dates
Period age = Period.between(birthday, today);
System.out.println("Age: " + age.getYears() + " years");
// Output: Age: 31 years
// Date arithmetic — returns a new instance since objects are immutable
LocalDate nextWeek = today.plusWeeks(1);
long daysUntil = ChronoUnit.DAYS.between(today, nextWeek);
System.out.println("Days until next week: " + daysUntil);
// Output: Days until next week: 7
// LocalDateTime: date + time
LocalDateTime now = LocalDateTime.now();
// DateTimeFormatter: formatting
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
System.out.println("Current: " + now.format(formatter));
// Output: Current: 2026-04-08 14:30
// Parsing a string to LocalDate
LocalDate parsed = LocalDate.parse("2026-12-25");
System.out.println("Christmas: " + parsed.getDayOfWeek());
// Output: Christmas: FRIDAY
}
}
All core classes in java.time (LocalDate, LocalDateTime, ZonedDateTime) are immutable and thread-safe. Operations like plusDays() and minusHours() always return new instances, completely eliminating the concurrency bugs that plagued SimpleDateFormat.
Performance-Related Changes
JDK 8 also introduced important changes at the JVM level beyond the API improvements.
PermGen to Metaspace: The legacy PermGen area was removed and replaced with Metaspace, which uses native memory. This eliminated the infamous OutOfMemoryError: PermGen space error in large-scale applications with extensive class metadata.
Fork/Join Improvements: The work distribution algorithm of the Fork/Join framework used internally by parallelStream() was optimized, improving the performance of parallel streams.
Nashorn JavaScript Engine: The Nashorn engine for running JavaScript on the JVM was introduced (later removed in JDK 15). At the time it was used for server-side scripting, but GraalVM has since taken over this role.
Summary
JDK 8 is a historic release that brought the functional programming paradigm to Java.
| Feature | Core Value |
|---|---|
| Lambda | Concise function passing, replaces anonymous classes |
| Stream API | Declarative data processing, easy parallelization |
| Optional | NPE prevention guaranteed through the type system |
| java.time | Immutable, thread-safe date handling |
| Metaspace | Eliminates PermGen OOM errors |
Java before and after JDK 8 can fairly be described as two different languages. Without lambdas and streams, modern Java code is neither readable nor writable. If you are not yet fully leveraging JDK 8 features, starting with Stream API and Optional in your production code is highly recommended.