Java 8 Features
Java 8 Features
Java 8, released in 2014, is the single most significant release in the language's history — more production code targets Java 8's baseline today than any other version, and nearly everything covered elsewhere in this series, lambda expressions, the Stream API, Optional, the modern Date and Time API, method references, arrived in this one release. What changed wasn't a single feature added to an already-complete language; it was Java's entire default style of writing everyday code, shifting from loops and mutable state toward expressions and immutable values.
What Changed in Java 8?
| Feature | What It Solves |
|---|---|
| Lambda expressions | Removes anonymous-class boilerplate for implementing single-method interfaces |
| Functional interfaces | Formalizes the single-abstract-method contract lambdas target |
| Stream API | Replaces manual loops with declarative filter, map, and collect pipelines |
| Method references | Shortens lambdas that only forward to an existing method |
| Optional | Makes "this might not have a value" explicit in a return type |
| Default and static interface methods | Lets an interface gain new methods without breaking existing implementers |
| New Date and Time API (java.time) | Replaces the mutable, thread-unsafe Date and Calendar classes |
| Base64 API | Adds encoding and decoding directly to the JDK, with no external library needed |
| Nashorn JavaScript engine | Ships a JavaScript engine directly inside the JVM (deprecated in Java 11, removed in Java 15) |
| CompletableFuture | Adds composable, callback-driven asynchronous programming to java.util.concurrent |
Why Java 8 Was a Turning Point
The shift is visible in the smallest possible example — passing behavior into a method used to mean writing an entire anonymous class just to supply one method body.
Before Java 8: After Java 8:
new Comparator<String>() { (a, b) -> a.compareTo(b)
public int compare(String a,
String b) {
return a.compareTo(b);
}
}
Six lines of ceremony collapse into one expression that says exactly what it does and nothing else — this single shift in how behavior gets passed around is what the rest of Java 8 builds on.
Interview tip: when asked to list Java 8 features, group them into three buckets instead of reciting a flat list — syntax (lambdas, method references), API (Streams, Optional), and infrastructure (java.time, Base64). Interviewers notice organized thinking faster than a longer list.
Filtering a list and summing a condition used to mean a loop with an accumulator variable, tracked and mutated by hand across every iteration.
1// File: BeforeJava8.java
2import java.util.*;
3
4public class BeforeJava8 {
5 record Order(String category, double amount) {}
6
7 public static void main(String[] args) {
8 List<Order> orders = List.of(
9 new Order("Electronics", 799.0),
10 new Order("Books", 450.0),
11 new Order("Electronics", 1499.0)
12 );
13
14 double total = 0;
15 for (Order order : orders) {
16 if (order.category().equals("Electronics")) {
17 total += order.amount();
18 }
19 }
20
21 System.out.println("Electronics total: " + total);
22 }
23}Output:
Electronics total: 2298.0
The same calculation as a stream pipeline reads as a description of the result, with lambdas and method references doing the work the loop and accumulator used to.
1// File: AfterJava8.java
2import java.util.*;
3import java.util.stream.*;
4
5public class AfterJava8 {
6 record Order(String category, double amount) {}
7
8 public static void main(String[] args) {
9 List<Order> orders = List.of(
10 new Order("Electronics", 799.0),
11 new Order("Books", 450.0),
12 new Order("Electronics", 1499.0)
13 );
14
15 double total = orders.stream()
16 .filter(order -> order.category().equals("Electronics"))
17 .mapToDouble(Order::amount)
18 .sum();
19
20 System.out.println("Electronics total: " + total);
21 }
22}Output:
Electronics total: 2298.0
Both versions compute the same total. The second one has no loop variable or running total for a reader to track by hand.
A Tour of Java 8's Core Features
Lambda Expressions and Functional Interfaces
A lambda implements a functional interface — an interface with exactly one abstract method — inline, without a class declaration. This is the foundation everything else in this list is built on, and it has its own dedicated articles in this series covering syntax, scoping rules, and the built-in Predicate, Function, Consumer, and Supplier interfaces in depth.
1// File: LambdaQuickLook.java
2import java.util.function.*;
3
4public class LambdaQuickLook {
5 public static void main(String[] args) {
6 Predicate<String> isLongName = name -> name.length() > 5;
7
8 System.out.println(isLongName.test("Ananya"));
9 }
10}Output:
true
The Stream API
A stream chains filtering, transforming, and collecting operations into one declarative pipeline over a collection, covered across its own dedicated series of articles for each operation.
1// File: StreamQuickLook.java
2import java.util.*;
3import java.util.stream.*;
4
5public class StreamQuickLook {
6 public static void main(String[] args) {
7 List<String> names = List.of("Rohit", "Ananya", "Vikram");
8
9 List<String> upperCaseNames = names.stream()
10 .map(String::toUpperCase)
11 .collect(Collectors.toList());
12
13 System.out.println(upperCaseNames);
14 }
15}Output:
[ROHIT, ANANYA, VIKRAM]
Optional
Optional puts "this might not have a value" directly into a method's return type, replacing a null a caller might forget to check.
1// File: OptionalQuickLook.java
2import java.util.*;
3
4public class OptionalQuickLook {
5 public static void main(String[] args) {
6 Optional<String> maybeName = Optional.ofNullable(null);
7
8 System.out.println(maybeName.orElse("Unknown"));
9 }
10}Output:
Unknown
Default and Static Methods in Interfaces
Before Java 8, adding a new method to an interface broke every class that already implemented it, since implementing an interface has always meant providing every one of its methods. A default method carries its own body, so existing implementers inherit it automatically without needing to change anything — this is exactly how the JDK added forEach() to Iterable and stream() to Collection without breaking the enormous number of classes that already implemented them.
1// File: DefaultMethodExample.java
2
3public class DefaultMethodExample {
4
5 interface Vehicle {
6 String getName();
7
8 // Default method - existing implementers get this for free,
9 // without needing to implement it themselves
10 default void honk() {
11 System.out.println(getName() + " says: Beep beep!");
12 }
13
14 // Static method - belongs to the interface itself, not to
15 // any particular implementation
16 static Vehicle basic(String name) {
17 return () -> name;
18 }
19 }
20
21 static class ElectricCar implements Vehicle {
22 public String getName() { return "Electric Car"; }
23
24 // Overriding the default is optional, not required
25 @Override
26 public void honk() {
27 System.out.println(getName() + " says: Silent beep");
28 }
29 }
30
31 public static void main(String[] args) {
32 Vehicle genericVehicle = Vehicle.basic("Bicycle");
33 genericVehicle.honk();
34
35 Vehicle electricCar = new ElectricCar();
36 electricCar.honk();
37 }
38}Output:
Bicycle says: Beep beep!
Electric Car says: Silent beep
genericVehicle is built from a lambda and never overrides honk(), so it falls back to the interface's own default implementation. electricCar overrides it explicitly, and its own version runs instead — default methods provide a fallback, not a fixed, unchangeable behavior.
The New Date and Time API
java.time replaces the mutable, thread-unsafe Date and Calendar classes with immutable, purpose-built types — LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Period, Duration, and DateTimeFormatter, each covered in its own dedicated article in this series.
1// File: DateTimeQuickLook.java
2import java.time.*;
3
4public class DateTimeQuickLook {
5 public static void main(String[] args) {
6 LocalDate today = LocalDate.of(2026, 8, 25);
7 LocalDate nextWeek = today.plusWeeks(1);
8
9 System.out.println(nextWeek);
10 }
11}Output:
2026-09-01
Method References
A method reference points directly at an existing method, replacing a lambda whose only job is to forward its arguments into that method.
1// File: MethodReferenceQuickLook.java
2import java.util.*;
3
4public class MethodReferenceQuickLook {
5 public static void main(String[] args) {
6 List<String> names = List.of("Rohit", "Ananya", "Vikram");
7
8 names.forEach(System.out::println);
9 }
10}Output:
Rohit
Ananya
Vikram
The Base64 API
Before Java 8, encoding or decoding Base64 meant relying on the unsupported, internal sun.misc.BASE64Encoder class, or pulling in an entire external library for one small utility. java.util.Base64 added a clean, standard, fully documented encoder and decoder directly to the JDK.
1// File: Base64Example.java
2import java.util.*;
3import java.nio.charset.*;
4
5public class Base64Example {
6 public static void main(String[] args) {
7 String original = "DevStackFlow";
8
9 String encoded = Base64.getEncoder().encodeToString(original.getBytes(StandardCharsets.UTF_8));
10 byte[] decodedBytes = Base64.getDecoder().decode(encoded);
11 String decoded = new String(decodedBytes, StandardCharsets.UTF_8);
12
13 System.out.println("Original: " + original);
14 System.out.println("Encoded length: " + encoded.length());
15 System.out.println("Decoded matches original: " + original.equals(decoded));
16 }
17}Output:
Original: DevStackFlow
Encoded length: 16
Decoded matches original: true
Real-World Example
A weekly sales report needs to filter transactions down to a specific date range, total them, identify the top seller safely even when no sales exist, and format the header through a pluggable interface — a compact scenario that uses LocalDate, the Stream API, Optional, a lambda-implemented interface, and a default method all in the same few lines.
1// File: SaleRecord.java
2import java.time.*;
3
4public record SaleRecord(String product, double amount, LocalDate saleDate) {}1// File: ReportFormatter.java
2
3@FunctionalInterface
4public interface ReportFormatter {
5 String formatHeader(String title);
6
7 // Default method - shared formatting logic every formatter gets for free
8 default String formatCurrency(double amount) {
9 return String.format("Rs.%.2f", amount);
10 }
11}1// File: WeeklySalesReport.java
2import java.time.*;
3import java.util.*;
4import java.util.stream.*;
5
6public class WeeklySalesReport {
7
8 public void generate(List<SaleRecord> sales, LocalDate weekStart, LocalDate weekEnd, ReportFormatter formatter) {
9 List<SaleRecord> thisWeek = sales.stream()
10 .filter(sale -> !sale.saleDate().isBefore(weekStart) && !sale.saleDate().isAfter(weekEnd))
11 .collect(Collectors.toList());
12
13 double total = thisWeek.stream()
14 .mapToDouble(SaleRecord::amount)
15 .sum();
16
17 Optional<SaleRecord> topSale = thisWeek.stream()
18 .max(Comparator.comparingDouble(SaleRecord::amount));
19
20 System.out.println(formatter.formatHeader("Weekly Sales Report"));
21 System.out.println("Total: " + formatter.formatCurrency(total));
22 System.out.println("Top sale: " + topSale
23 .map(sale -> sale.product() + " - " + formatter.formatCurrency(sale.amount()))
24 .orElse("No sales this week"));
25 }
26}1// File: SalesReportDemo.java
2import java.time.*;
3import java.util.*;
4
5public class SalesReportDemo {
6 public static void main(String[] args) {
7 List<SaleRecord> sales = List.of(
8 new SaleRecord("Wireless Mouse", 799.0, LocalDate.of(2026, 8, 24)),
9 new SaleRecord("Keyboard", 1499.0, LocalDate.of(2026, 8, 26)),
10 new SaleRecord("Monitor", 8999.0, LocalDate.of(2026, 8, 20))
11 );
12
13 ReportFormatter formatter = title -> "=== " + title + " ===";
14
15 WeeklySalesReport report = new WeeklySalesReport();
16 report.generate(sales, LocalDate.of(2026, 8, 23), LocalDate.of(2026, 8, 29), formatter);
17 }
18}Output:
=== Weekly Sales Report ===
Total: Rs.2298.00
Top sale: Keyboard - Rs.1499.00
The Monitor sale from August 20th falls outside the requested week and is correctly excluded by the filter() step, leaving only the two in-range sales to total and rank. During code reviews, seniors commonly point to exactly this kind of report generator as the moment Java 8 features stop feeling like separate, disconnected topics and start reading as one coherent style — a date range check, a stream pipeline, an Optional-safe lookup, and a lambda-implemented interface each doing exactly one job, instead of one long method mixing all four concerns together by hand.
Combining Java 8 Features With Each Other
Every feature in this article composes with the others by design — Stream.filter() and map() take a Predicate and a Function, both implemented as lambdas or method references; Stream.max() and similar terminal operations return an Optional to handle the empty case safely; and java.time values flow through stream pipelines exactly like any other object, filtered and compared the same way. The dedicated articles throughout this series on Lambda Expressions, Functional Interfaces, Streams, Optional, and the Date and Time API each go deep into one of these pieces — this article exists to show how naturally they fit together once each one is individually familiar.
Best Practices
Reach for the dedicated article on each feature the moment a real task needs to go past a one-line example — Lambda Expressions, Functional Interfaces, Streams, Optional, and the Date and Time API are each covered in full depth elsewhere in this series.
Use default methods for genuine backward-compatible interface evolution, not as a convenient place to stash shared logic that would fit more naturally in a regular helper class.
Prefer java.time and java.util.Base64 over any pre-Java-8 workaround or third-party dependency that existed solely to fill the gap those two features closed.
Treat Java 8 as the baseline the rest of this Modern Java section builds on — var, switch expressions, and records all assume a reader who is already comfortable with lambdas and streams.
Common Mistakes
Forcing a stream pipeline onto something small enough that a plain expression would be clearer adds setup overhead for no real readability benefit.
1// File: OverusingStreamsMistake.java
2import java.util.*;
3
4public class OverusingStreamsMistake {
5 public static void main(String[] args) {
6 List<Integer> twoNumbers = List.of(3, 7);
7
8 // WRONG INTENT - forcing a stream pipeline onto two elements adds
9 // pipeline setup overhead for no readability benefit at this scale
10 int sumViaStream = twoNumbers.stream().mapToInt(Integer::intValue).sum();
11
12 // CORRECT - a plain expression is clearer for something this small
13 int sumDirect = twoNumbers.get(0) + twoNumbers.get(1);
14
15 System.out.println("Via stream: " + sumViaStream);
16 System.out.println("Direct: " + sumDirect);
17 }
18}Output:
Via stream: 10
Direct: 10
Assuming a static interface method behaves like a default method — inherited and overridable by implementing classes — overlooks that static methods belong to the interface itself and are never inherited at all.
1// File: DefaultVsStaticMistake.java
2
3public class DefaultVsStaticMistake {
4
5 interface Greeter {
6 default String greet() { return "Hello from default"; }
7 static String staticGreet() { return "Hello from static"; }
8 }
9
10 static class CustomGreeter implements Greeter {
11 // Overriding a default method works fine
12 @Override
13 public String greet() { return "Hello from CustomGreeter"; }
14
15 // A static method cannot be overridden - this is a brand new,
16 // unrelated static method, not an override of Greeter.staticGreet()
17 static String staticGreet() { return "Hello from CustomGreeter's own static method"; }
18 }
19
20 public static void main(String[] args) {
21 Greeter greeter = new CustomGreeter();
22 System.out.println(greeter.greet());
23
24 System.out.println(Greeter.staticGreet());
25 System.out.println(CustomGreeter.staticGreet());
26 }
27}Output:
Hello from CustomGreeter
Hello from static
Hello from CustomGreeter's own static method
Assuming every Java 8 feature is a strict drop-in replacement with no new behavior of its own is a subtler mistake worth avoiding — Optional is not simply a nullable reference with extra syntax, and a stream pipeline is not automatically faster than an equivalent loop. Each dedicated article in this series covers the specific mistakes that come from treating its feature as a plain syntax upgrade rather than a genuinely different tool.
Interview Questions
Q1. What were the major features introduced in Java 8?
Lambda expressions and functional interfaces, the Stream API, Optional, default and static interface methods, the new java.time Date and Time API, method references, and the java.util.Base64 API, along with CompletableFuture for composable asynchronous programming. Interviewers typically use this as an opening question to see how comprehensively a candidate can list the release's scope before diving into any one feature in depth.
Q2. Why was Java 8 considered such a significant release compared to previous versions?
Earlier releases mostly added individual classes or syntax conveniences, while Java 8 changed the language's default style of writing everyday code — from imperative loops and mutable state toward declarative, functional-style expressions built around lambdas and streams. It also fixed two long-standing, widely felt design problems at once: the awkward, unsafe legacy date API and the boilerplate required to pass behavior as a value. The nuance interviewers listen for here is whether you can name what actually changed structurally, not just recite the feature list.
Q3. What problem do default methods solve, and how did the JDK itself use them?
Default methods let an interface gain a new method with a body, which existing implementing classes inherit automatically without needing any code changes — solving the problem that adding any new method to an interface used to break every class that already implemented it. The JDK used this directly to add forEach() to Iterable and stream() to Collection in Java 8 without breaking the vast number of existing classes across the ecosystem that already implemented those interfaces. Interviewers at product companies often follow up by asking why this couldn't have been solved with abstract classes instead — the answer is that a class can extend only one class but implement many interfaces.
Q4. Can a static method on an interface be overridden by an implementing class?
No. A static method belongs to the interface itself, not to any implementing class, so it is never inherited and cannot be overridden. A class can declare its own static method with the same name and signature, but that is an entirely separate, unrelated method — not a polymorphic override, which is exactly the distinction product-based interviews often probe by asking what happens when both exist.
Q5. What is the relationship between lambda expressions and functional interfaces?
A functional interface defines the contract — the single abstract method's parameter types and return type — and a lambda expression provides an implementation of that contract inline, without a class declaration. A lambda cannot exist without a functional interface as its target type, and understanding this relationship is what makes the rest of java.util.function and the Stream API make sense as a coherent system rather than a list of separate APIs to memorize.
Q6. What existed before java.util.Base64, and why was adding it to the JDK worthwhile?
Before Java 8, encoding or decoding Base64 meant using the unsupported, internal sun.misc.BASE64Encoder and BASE64Decoder classes, which generated compiler warnings and were never part of any official public API, or bringing in an external library like Apache Commons Codec for a task that conceptually belongs in the standard library. java.util.Base64 gave every Java application a clean, documented, dependency-free way to perform an extremely common operation — a smaller question, but service-based interviewers use it to check whether a candidate actually knows the standard library beyond collections and streams.
FAQs
Is Java 8 still supported and widely used today?
Yes. Java 8 remains one of the most widely deployed versions in production, and Oracle and other vendors continue offering extended support for it, which is exactly why so much real-world code — and this article's comparison to legacy alternatives — still matters.
Do I need to learn every Java 8 feature before moving on to Java 11, 17, or 21 features?
A working understanding of lambdas, streams, and Optional is genuinely necessary, since later releases build directly on those foundations — var, switch expressions, and pattern matching all assume familiarity with the functional style Java 8 introduced. Deep expertise in every corner, like the full Base64 API surface, is not required before moving forward.
What is the difference between a default method and an abstract method on the same interface?
An abstract method has no body and must be implemented by every class that implements the interface. A default method has a body and is inherited automatically, with implementation left optional for each implementing class. An interface can freely mix both, exactly as ReportFormatter in this article's real-world example does.
Was the Stream API the only way Java 8 improved working with collections?
No. Beyond streams, Java 8 added default methods like forEach(), removeIf(), and replaceAll() directly to existing collection interfaces, along with new methods on Map like computeIfAbsent() and merge(), all improving everyday collection work without requiring a full stream pipeline for simple cases.
What happened to the Nashorn JavaScript engine mentioned as a Java 8 feature?
Nashorn was deprecated in Java 11 and fully removed in Java 15, since it struggled to keep pace with the rapidly evolving JavaScript language specification and saw limited real-world adoption. It is a genuinely notable piece of Java 8 history, but not something current code should depend on.
Can a class implement two interfaces that both have default methods with the same signature?
Yes, but the compiler forces the implementing class to resolve the conflict explicitly by overriding the method itself, rather than guessing which interface's default should win. Inside that override, the class can call either parent's version directly using InterfaceName.super.methodName().
Is CompletableFuture part of the Stream API?
No. CompletableFuture lives in java.util.concurrent and is part of Java 8's concurrency improvements, used for composing asynchronous operations with callbacks — a genuinely separate feature from the Stream API, even though both were introduced in the same release and both lean heavily on lambdas.
Summary
Java 8 is less a single feature and more the moment Java adopted an entirely different default shape for everyday code — lambdas and functional interfaces gave behavior a way to travel as a value, streams turned loops into declarative pipelines, Optional gave absence a real type, default methods let interfaces evolve safely, and java.time finally replaced a genuinely broken legacy API.
The weekly sales report example above is worth remembering as the shape this all takes in real code: small, focused pieces — a filter, a stream pipeline, an Optional-safe lookup, a lambda-implemented interface — composed together rather than one long method doing everything by hand. Every dedicated article in this series goes deeper into exactly one of these pieces; this one is the map showing how they all fit.
What to Read Next
Learn what's new in the Java 11 long-term support release.