Java 8 Complete Feature Guide
A practical, detailed guide to the release that changed modern Java development. Learn lambdas, functional interfaces, streams, Optional, the Date and Time API, CompletableFuture, collection enhancements, common mistakes, and senior-level interview questions.
1What Changed in Java 8?
Java 8 brought functional-style programming to Java while retaining its object-oriented foundation. The major changes include lambda expressions, method references, functional interfaces, default methods, streams, Optional, a modern Date and Time API, and asynchronous composition through CompletableFuture.
2Lambda Expressions
A lambda expression provides an implementation for the single abstract method of a functional interface.
(parameters) -> expression
(parameters) -> {
statements;
}Before Java 8
Collections.sort(employees, new Comparator<Employee>() {
@Override
public int compare(Employee first, Employee second) {
return first.getName().compareTo(second.getName());
}
});Using a lambda
employees.sort((first, second) ->
first.getName().compareTo(second.getName()));Variable capture
int minimumSalary = 50_000; // Effectively final
employees.stream()
.filter(employee -> employee.getSalary() > minimumSalary)
.forEach(System.out::println);3Functional Interfaces
@FunctionalInterface
interface SalaryRule {
boolean test(Employee employee);
}| Interface | Input and output | Method | Use |
|---|---|---|---|
| Predicate<T> | T to boolean | test() | Filtering |
| Function<T,R> | T to R | apply() | Transformation |
| Consumer<T> | T to void | accept() | Performing an action |
| Supplier<T> | No input to T | get() | Lazy object creation |
| UnaryOperator<T> | T to T | apply() | Same-type transformation |
| BinaryOperator<T> | T and T to T | apply() | Combining values |
Predicate<Employee> highPaid =
employee -> employee.getSalary() > 100_000;
Function<Employee, String> employeeName = Employee::getName;
Consumer<Employee> printEmployee = System.out::println;
Supplier<List<Employee>> listFactory = ArrayList::new;
Predicate<Employee> highPaidITEmployee = highPaid.and(
employee -> "IT".equals(employee.getDepartment()));4Method and Constructor References
Static method
numbers.stream()
.reduce(Integer::sum);Bound instance method
employees.forEach(
System.out::println);Unbound instance method
employees.stream()
.map(Employee::getName);Constructor reference
Supplier<List<Employee>> factory =
ArrayList::new;5Default and Static Interface Methods
interface Auditable {
default String auditMessage() {
return "Audited at " + Instant.now();
}
static boolean isValid(String value) {
return value != null && !value.trim().isEmpty();
}
}Conflict-resolution rules
- A concrete class method wins over an interface default method.
- A method from a more specific child interface wins over its parent interface.
- If unrelated interfaces declare conflicting defaults, the implementing class must override the method.
interface A {
default void print() { System.out.println("A"); }
}
interface B {
default void print() { System.out.println("B"); }
}
class Example implements A, B {
@Override
public void print() {
A.super.print();
}
}6Stream API Deep Dive
List<String> names = employees.stream()
.filter(employee -> employee.getSalary() > 80_000)
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.map(Employee::getName)
.collect(Collectors.toList());Stateless
filter, map and flatMap process elements independently.
Stateful
distinct and sorted may retain information about previously seen elements.
Terminal
collect, reduce, count, match, find and forEach trigger execution.
map() versus flatMap()
List<String> uniqueSkills = employees.stream()
.flatMap(employee -> employee.getSkills().stream())
.map(String::toUpperCase)
.distinct()
.sorted()
.collect(Collectors.toList());reduce()
double totalSalary = employees.stream()
.map(Employee::getSalary)
.reduce(0.0, Double::sum);
int total = IntStream.rangeClosed(1, 100).sum();Short-circuit operations
boolean anyHighPaidEmployee = employees.stream()
.anyMatch(employee -> employee.getSalary() > 200_000);
Optional<Employee> firstITEmployee = employees.stream()
.filter(employee -> "IT".equals(employee.getDepartment()))
.findFirst();7Collectors and Employee Examples
Group by department
Map<String, List<Employee>> byDepartment =
employees.stream().collect(Collectors.groupingBy(
Employee::getDepartment));Count per department
Map<String, Long> countByDepartment =
employees.stream().collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()));Maximum salary employee
Map<String, Optional<Employee>> highestPaid =
employees.stream().collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparing(
Employee::getSalary))));Average salary
Map<String, Double> averageSalary =
employees.stream().collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(
Employee::getSalary)));Third-highest distinct salary, department-wise
employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment))
.forEach((department, employeeList) -> {
Double salary = employeeList.stream()
.map(Employee::getSalary)
.distinct()
.sorted(Comparator.reverseOrder())
.skip(2)
.findFirst()
.orElse(null);
System.out.println(department + " -> " + salary);
});Why distinct? The third-highest salary normally means the third distinct salary. Duplicate salary values should share the same rank.
toMap() with duplicate-key handling
Map<String, Employee> employeeByName = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Function.identity(),
BinaryOperator.maxBy(
Comparator.comparing(Employee::getSalary))));8Parallel Streams and Spliterator
double totalSalary = employees.parallelStream()
.mapToDouble(Employee::getSalary)
.sum();Consider parallel streams when
- The dataset is large and in memory.
- Work is CPU-intensive and independent.
- The source can be split efficiently.
- Reduction is associative.
Avoid them when
- Operations perform blocking I/O.
- The collection is small.
- Shared mutable state is involved.
- Ordering costs dominate.
- Common-pool contention is risky.
Spliterator supports both traversal and partitioning. Characteristics such as ORDERED, DISTINCT, SORTED, SIZED and SUBSIZED help the stream framework understand a data source.
9Optional
Optional<Employee> employeeOptional = repository.findById(id);
String name = employeeOptional
.filter(employee -> employee.getSalary() > 50_000)
.map(Employee::getName)
.orElse("Unknown");orElse()
value.orElse(createDefault());
// createDefault() is called eagerlyorElseGet()
value.orElseGet(this::createDefault);
// Supplier runs only when emptyFlatten nested Optional values
Optional<String> city = employeeOptional
.flatMap(Employee::getAddress)
.map(Address::getCity);10Modern Date and Time API
| Type | Purpose |
|---|---|
| LocalDate | Date without time or zone |
| LocalTime | Time without date or zone |
| LocalDateTime | Date and time without a zone |
| Instant | A point on the UTC timeline |
| ZonedDateTime | Date and time with a region-based zone |
| OffsetDateTime | Date and time with a numeric UTC offset |
| Period | Date-based amount |
| Duration | Time-based amount |
LocalDate joiningDate = LocalDate.of(2020, Month.JANUARY, 15);
long years = ChronoUnit.YEARS.between(
joiningDate, LocalDate.now());
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("dd-MM-yyyy");
String formattedDate = joiningDate.format(formatter);
Instant currentInstant = Instant.now();
ZonedDateTime indiaTime = currentInstant.atZone(
ZoneId.of("Asia/Kolkata"));11CompletableFuture
CompletableFuture<Employee> employeeFuture =
CompletableFuture.supplyAsync(
() -> loadEmployee(id), executor);
CompletableFuture<String> result = employeeFuture
.thenApply(Employee::getName)
.exceptionally(exception -> "Unknown");Combine independent calls
CompletableFuture<Profile> profileFuture =
CompletableFuture.supplyAsync(this::loadProfile, executor);
CompletableFuture<Salary> salaryFuture =
CompletableFuture.supplyAsync(this::loadSalary, executor);
CompletableFuture<EmployeeView> viewFuture =
profileFuture.thenCombine(
salaryFuture, EmployeeView::new);| Method | Purpose |
|---|---|
| thenApply() | Transform a completed result |
| thenCompose() | Chain and flatten a dependent future |
| thenCombine() | Combine two independent results |
| allOf() / anyOf() | Coordinate multiple futures |
| handle() | Process either a result or exception |
| exceptionally() | Recover from a failure |
12Collection, Map and Comparator Improvements
Collection operations
employees.forEach(System.out::println);
employees.removeIf(employee -> !employee.isActive());
employees.replaceAll(this::normalize);Map operations
employeeMap.forEach((id, employee) ->
System.out.println(employee));
Employee employee = employeeMap.getOrDefault(id, defaultEmployee);
employeeMap.putIfAbsent(id, newEmployee);
departmentEmployees
.computeIfAbsent(department, key -> new ArrayList<>())
.add(employee);
wordCount.merge(word, 1, Integer::sum);Put Map values into another list
List<Employee> employeeList =
new ArrayList<>(employeeMap.values());Comparator composition
Comparator<Employee> employeeComparator =
Comparator.comparing(Employee::getDepartment)
.thenComparing(
Employee::getSalary,
Comparator.reverseOrder())
.thenComparing(
Employee::getName,
Comparator.nullsLast(
String.CASE_INSENSITIVE_ORDER));HashSet.add() return value
Set<Integer> seen = new HashSet<>();
numbers.stream()
.filter(number -> !seen.add(number))
.forEach(System.out::println);HashSet.add() returns true when the element is inserted and false when an equal element already exists. Therefore, !seen.add(number) selects duplicates.
13Other Important Java 8 Features
Base64
String encoded = Base64.getEncoder()
.encodeToString("Java 8".getBytes(
StandardCharsets.UTF_8));
String decoded = new String(
Base64.getDecoder().decode(encoded),
StandardCharsets.UTF_8);StringJoiner
StringJoiner joiner =
new StringJoiner(", ", "[", "]");
joiner.add("Java").add("Spring");Repeatable annotations
@Repeatable(Roles.class)
@interface Role { String value(); }
@interface Roles { Role[] value(); }
@Role("ADMIN")
@Role("AUDITOR")
class UserService {}Files.lines()
try (Stream<String> lines =
Files.lines(logFilePath)) {
long errors = lines
.filter(line -> line.contains("ERROR"))
.count();
}- Metaspace: Native-memory Metaspace replaced PermGen for class metadata.
- Target-type inference: Generic type inference was improved in more invocation contexts.
- Parameter names: Reflection can read parameter names when compiled using
-parameters. - Concurrency: Java 8 added classes such as StampedLock, LongAdder and LongAccumulator.
- Nashorn: Java 8 included a JavaScript engine, but it should not be chosen for modern application designs.
14Common Java 8 Mistakes
Reusing a stream
A stream is consumed after a terminal operation and cannot be reused.
Shared mutation
Avoid updating external mutable collections from a stream pipeline.
Optional.get()
Prefer map, flatMap, orElseGet or orElseThrow.
Assuming null safety
Streams do not make null elements or mappers automatically safe.
Incorrect time type
Do not use LocalDateTime when you need a global timestamp.
Blind parallelization
Measure before using parallel streams and avoid blocking calls.
15Java 8 Interview Questions
Why can a functional interface contain default and static methods?
The single-abstract-method rule counts abstract instance methods. Default and static methods already have implementations.
What is the difference between a collection and a stream?
A collection stores elements and supports repeated traversal. A stream is a consumable, normally lazy computation pipeline using internal iteration.
What is the difference between map() and flatMap()?
map() transforms each input into one result. flatMap() transforms inputs into streams and flattens those streams into one pipeline.
Why must reduce operations be associative?
Parallel execution can combine partitions in different groupings. Associativity ensures those groupings produce the same result.
findFirst() versus findAny()?
findFirst() respects encounter order. findAny() allows more freedom and may be useful when any matching result is acceptable.
orElse() versus orElseGet()?
orElse() evaluates its argument eagerly. orElseGet() invokes its supplier only when the Optional is empty.
thenApply() versus thenCompose()?
thenApply() maps a value to another value. thenCompose() chains a function that returns a future and flattens the nested future.
Why can parallelStream() be risky in a web application?
It commonly uses the shared ForkJoin common pool. Blocking work, request concurrency, small tasks and ordering requirements can create unpredictable performance.
What is the difference between intermediate and terminal stream operations?
Intermediate operations return another stream and are normally lazy. Terminal operations produce a result or side effect and trigger execution.
What happens when toMap() receives duplicate keys?
It throws an exception unless a merge function is supplied to decide how duplicate values should be combined or selected.