Java 8 features with examples
12 min readUpdated
Java 8 changed how Java code is written more than any release before it. Lambdas and streams replaced many loops and anonymous classes, Optional made missing values explicit, and a new date-time API fixed the old Date class. Here is each feature with a small program you can run.
The features at a glance
| Feature | What it gives you |
|---|---|
| Lambda expressions | Short functions you can pass around: (a, b) -> a + b |
| Functional interfaces | Function, Predicate, Consumer, Supplier and friends in java.util.function |
| Method references | String::length, System.out::println: a lambda that just calls a method |
| Stream API | Filter, transform, group and reduce collections in a pipeline |
Optional | A box that is either empty or holds a value, instead of null |
| Default and static interface methods | Interfaces can have methods with bodies |
| Date and time API | LocalDate, LocalDateTime, Duration, Period (immutable and clear) |
Lambda expressions and functional interfaces
A lambda is an implementation of a functional interface, an interface with exactly one abstract method. Instead of writing an anonymous class, you write only the parameters and the body.
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class LambdaBasics {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
Function<String, Integer> length = s -> s.length();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
Supplier<String> greeting = () -> "Hello, Java 8";
System.out.println(isEven.test(10));
System.out.println(length.apply("lambda"));
System.out.println(add.apply(3, 4));
System.out.println(greeting.get());
System.out.println(isEven.negate().test(10)); // predicates can be combined
}
}Output
true 6 7 Hello, Java 8 false
Method references
When a lambda only calls an existing method, a method reference says the same thing more briefly: s -> s.toUpperCase() becomes String::toUpperCase.
| Kind | Example | Same as |
|---|---|---|
| Static method | Integer::parseInt | s -> Integer.parseInt(s) |
| Method of a given object | System.out::println | x -> System.out.println(x) |
| Method of the parameter | String::length | s -> s.length() |
| Constructor | ArrayList::new | () -> new ArrayList<>() |
The Stream API
A stream is a pipeline over data: a source, zero or more intermediate operations (filter, map, sorted, lazy) and one terminal operation (collect, count, forEach, reduce) that runs it. The original collection is never changed.
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class StreamPipeline {
record Student(String name, String branch, int marks) {}
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Asha", "CSE", 88), new Student("Ravi", "ECE", 72),
new Student("Meera", "CSE", 95), new Student("John", "ECE", 64),
new Student("Sara", "MECH", 79));
List<String> toppers = students.stream()
.filter(s -> s.marks() >= 75)
.sorted((a, b) -> b.marks() - a.marks())
.map(Student::name)
.collect(Collectors.toList());
System.out.println(toppers);
double average = students.stream().mapToInt(Student::marks).average().orElse(0);
System.out.println(average);
Map<String, List<String>> byBranch = students.stream()
.collect(Collectors.groupingBy(Student::branch, TreeMap::new,
Collectors.mapping(Student::name, Collectors.toList())));
System.out.println(byBranch);
}
}Output
[Meera, Asha, Sara]
79.6
{CSE=[Asha, Meera], ECE=[Ravi, John], MECH=[Sara]}Streams are lazy: nothing runs until the terminal operation. A stream can be used only once; create a new one for a second pipeline.
Optional
Optional<T> makes "maybe no value" part of a method's return type, so callers cannot forget to handle it. Use it for return values, not for fields or parameters.
import java.util.Map;
import java.util.Optional;
public class OptionalDemo {
static final Map<Integer, String> USERS = Map.of(1, "asha", 2, "ravi");
static Optional<String> findUser(int id) {
return Optional.ofNullable(USERS.get(id));
}
public static void main(String[] args) {
System.out.println(findUser(1).map(String::toUpperCase).orElse("GUEST"));
System.out.println(findUser(9).map(String::toUpperCase).orElse("GUEST"));
System.out.println(findUser(2).isPresent());
findUser(1).ifPresent(name -> System.out.println("Welcome, " + name));
}
}Output
ASHA GUEST true Welcome, asha
Default and static methods in interfaces
Before Java 8, adding a method to an interface broke every class that implemented it. A default method has a body, so existing classes inherit it; that is how List gained sort() and Iterable gained forEach().
public class DefaultMethods {
interface Greeter {
String name();
default String greet() { // inherited by every implementation
return "Hello, " + name() + "!";
}
static Greeter of(String name) { // a helper on the interface itself
return () -> name;
}
}
public static void main(String[] args) {
Greeter g = Greeter.of("Meera");
System.out.println(g.greet());
}
}Output
Hello, Meera!
The date and time API
java.time replaced the confusing, mutable Date and Calendar. Its classes are immutable and say exactly what they hold: a date, a time, a date-time, an instant, a duration.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
public class DateTimeDemo {
public static void main(String[] args) {
LocalDate joined = LocalDate.of(2024, 7, 15);
LocalDate review = joined.plusMonths(6);
System.out.println(review);
System.out.println(review.getDayOfWeek() == DayOfWeek.WEDNESDAY);
Period gap = Period.between(LocalDate.of(2023, 1, 10), LocalDate.of(2025, 4, 5));
System.out.println(gap.getYears() + " years, " + gap.getMonths() + " months, " + gap.getDays() + " days");
System.out.println(ChronoUnit.DAYS.between(joined, review));
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MMM yyyy");
System.out.println(joined.format(fmt));
}
}Output
2025-01-15 true 2 years, 2 months, 26 days 184 15 Jul 2024
Java 8 interview questions
What is a functional interface?
An interface with exactly one abstract method, which a lambda can implement. @FunctionalInterface makes the compiler check it. Default and static methods do not count.
What is the difference between intermediate and terminal operations?
Intermediate operations (filter, map, sorted) return a new stream and are lazy. A terminal operation (collect, count, forEach, reduce) produces a result and runs the whole pipeline.
What is the difference between map() and flatMap()?
map() turns each element into one element. flatMap() turns each element into a stream and flattens them into one stream.
What is the difference between orElse() and orElseGet()?
orElse(x) always evaluates x, even when the value is present. orElseGet(() -> x) evaluates it only when the Optional is empty, which matters if building x is expensive.
Why must variables used inside a lambda be effectively final?
The lambda may run later, possibly on another thread, so it captures a copy of the value. Allowing the variable to change afterwards would make the copy and the variable disagree.
What are the new date-time classes, and why are they better?
LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration and Period. They are immutable and thread safe, months start at 1, and each class holds exactly one kind of value.
Frequently asked questions
What are the main features of Java 8?
Lambda expressions, functional interfaces, method references, the Stream API, Optional, default and static interface methods, and the java.time date and time API.
Is Java 8 still used?
Yes, in many older projects, but new projects usually use Java 17 or 21. Everything in Java 8 is still part of later versions, so these features are worth learning first.
Are streams faster than loops?
Not usually; for simple work a loop is about as fast or faster. Streams are chosen for readability. Parallel streams can help for large, CPU-heavy work, but measure first.
Learn it step by step
- Functional interfaces and lambdas: a first look
- The java.util.function package
- What is a method reference?
- Source, intermediate and terminal operations
- Group data with groupingBy
- Why Optional exists
- default, static and private methods in interfaces
- Dates with LocalDate
- Intermediate vs Terminal Operations and Laziness
Practise this, step by step
Short lessons with quizzes, practice and interview questions, from your first program to job-ready.