Lambda Expressions↓Functional Interfaces↓Stream API↓OptionalJava 8 Features: Lambda Expressions, Streams, Functional Interfaces, and Optional
Direct Answer & Core Concept
ava 8 introduced several language and library improvements that changed the way Java applications are written. Among the most important additions are Lambda Expressions, Functional Interfaces, Streams, and Optional.
These features made Java code more concise and provided better support for functional-style programming. Instead of writing large amounts of boilerplate code, developers can express operations such as filtering, sorting, mapping, and processing collections more directly.
In this tutorial, you will learn the core Java 8 features, how they work together, and when to use each one.
Detailed Explanation & Mechanics
What Is Java 8?
Java 8 was a major Java release that introduced functional programming capabilities while keeping Java's object-oriented programming model.
Some important Java 8 features include:
Lambda expressions
Functional interfaces
Stream API
OptionalMethod references
Default methods in interfaces
Static methods in interfaces
New Date and Time API
Improved
forEach()supportCompletableFuture
The four features covered here are particularly important because they are commonly used together.
1. Lambda Expressions in Java
A lambda expression is a short way of representing an implementation of a functional interface.
Before Java 8, developers often needed anonymous inner classes for small pieces of behavior.
For example:
Runnable task = new Runnable() {
@Override public void run() {
System.out.println("Task running");
}
};With a lambda expression:
Runnable task =() -> {
System.out.println("Task running");
};The lambda removes unnecessary boilerplate and makes the intention easier to see.
Lambda Expression Syntax
The general syntax is:
(parameters) -> expressionor:
(parameters) -> {
statements;
}For example:
(int a, int b) -> a + bThis lambda accepts two integers and returns their sum.
Lambda with One Parameter
When there is one parameter, parentheses can often be omitted:
name -> System.out.println(name)This is equivalent to:
(name) -> System.out.println(name)For example:
List<String> names = List.of("Amit", "Riya", "Tanmoy");
names.forEach(name -> System.out.println(name));Output:
AmitRiyaTanmoyLambda with Multiple Parameters
Multiple parameters require parentheses:
(a, b) -> a + bFor example:
BinaryOperator<Integer> add =(a, b) -> a + b;
System.out.println(add.apply(10, 20));Output:
302. Functional Interfaces
A functional interface is an interface that has exactly one abstract method.
It can contain default and static methods as well, but it must have only one abstract method.
Example:
@FunctionalInterfaceinterface Calculator {
int calculate(int a, int b);
}A lambda can provide the implementation:
Calculator calculator =(a, b) -> a + b;
System.out.println( calculator.calculate(10, 20));Output:
30The functional interface provides the target type for the lambda.
Why Functional Interfaces Matter
A lambda expression does not exist by itself as an ordinary standalone object with an arbitrary type.
It needs a compatible functional interface as its target type.
Conceptually:
Lambda Expression↓Functional Interface↓Implementation of its abstract methodFor example:
Runnable task =() -> System.out.println("Hello");Runnable supplies the functional-interface type.
Common Built-In Functional Interfaces
Java provides several useful functional interfaces in java.util.function.
| Interface | Main Method | Purpose |
|---|---|---|
Predicate<T> | test() | Tests a condition |
Function<T,R> | apply() | Converts one value to another |
Consumer<T> | accept() | Consumes a value |
Supplier<T> | get() | Supplies a value |
UnaryOperator<T> | apply() | Same type → same type |
BinaryOperator<T> | apply() | Two same-type values → same type |
Predicate
A Predicate represents a condition that returns true or false.
Predicate<Integer> isEven = number -> number % 2 == 0;
System.out.println(isEven.test(10));Output:
truePredicates are commonly used with streams.
Function
A Function converts one value into another.
Function<String, Integer> length = text -> text.length();
System.out.println(length.apply("Java"));Output:
4Consumer
A Consumer accepts a value but does not return a result.
Consumer<String> printer = text -> System.out.println(text);
printer.accept("Hello Java");Output:
Hello JavaSupplier
A Supplier produces a value without taking an input.
Supplier<String> message =() -> "Hello Java";
System.out.println(message.get());Output:
Hello Java3. Stream API in Java
The Stream API was introduced in Java 8 for processing sequences of data in a declarative style.
A stream is not a collection. Instead, it represents a pipeline through which data can be processed.
For example:
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);You can create a stream:
numbers.stream()and perform operations on it.
Basic Stream Example
Suppose we want only even numbers.
Traditional loop:
for (Integer number : numbers) {
if (number % 2 == 0) {
System.out.println(number);
}
}Using streams:
numbers.stream() .filter(number -> number % 2 == 0) .forEach(System.out::println);Output:
246The stream version describes the operation more directly:
numbers↓filter even numbers↓print resultStream Pipeline
A stream pipeline generally consists of:
Source↓Intermediate Operations↓Terminal OperationExample:
numbers.stream() .filter(n -> n > 3) .map(n -> n * 2) .forEach(System.out::println);Here:
numbersis the sourcefilter()is an intermediate operationmap()is an intermediate operationforEach()is a terminal operation
Intermediate Operations
Intermediate operations transform or filter a stream and return another stream.
Common examples include:
filter()map()flatMap()sorted()distinct()limit()skip()peek()They are generally lazy, meaning they do not process the data until a terminal operation is reached.
filter()
filter() keeps elements that satisfy a condition.
List<Integer> numbers = List.of(10, 15, 20, 25, 30);
numbers.stream() .filter(n -> n >= 20) .forEach(System.out::println);Output:
202530map()
map() transforms each element.
List<String> names = List.of("java", "spring", "mysql");
names.stream() .map(String::toUpperCase) .forEach(System.out::println);Output:
JAVASPRINGMYSQLsorted()
sorted() sorts stream elements according to their natural ordering or a supplied comparator.
List<Integer> numbers = List.of(50, 10, 40, 20, 30);
numbers.stream() .sorted() .forEach(System.out::println);Output:
1020304050distinct()
distinct() removes duplicate elements according to their equality semantics.
List<Integer> numbers = List.of(10, 20, 10, 30, 20);
numbers.stream() .distinct() .forEach(System.out::println);Output:
102030limit()
limit() restricts the number of elements.
List<Integer> numbers = List.of(10, 20, 30, 40, 50);
numbers.stream() .limit(3) .forEach(System.out::println);Output:
102030Terminal Operations
A terminal operation produces a final result or side effect and causes the stream pipeline to execute.
Common terminal operations include:
forEach()collect()reduce()count()min()max()findFirst()findAny()anyMatch()allMatch()noneMatch()collect()
collect() is commonly used to gather stream results into a collection or another result structure.
List<String> result = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList());For modern Java versions, you may also encounter:
List<String> result = names.stream() .filter(name -> name.length() > 4) .toList();The exact API available depends on the Java version being used. Stream.toList() was added after Java 8, so Java 8 code commonly uses Collectors.toList().
count()
count() returns the number of elements.
long count = numbers.stream() .filter(n -> n > 20) .count();
System.out.println(count);reduce()
reduce() combines stream elements into a single result.
For example:
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream() .reduce(0,(a, b) -> a + b);
System.out.println(sum);Output:
15A method reference can make this shorter:
int sum = numbers.stream() .reduce(0, Integer::sum);4. Optional in Java 8
Optional is a container that may or may not contain a non-null value.
It was introduced to make the absence of a value explicit and reduce some common null-handling problems.
Instead of:
String name = findName();
if (name != null) {
System.out.println(name);
}you can represent the result as:
Optional<String> name = findName();The important idea is that Optional represents:
Value exists ORNo value existsCreating Optional Objects
Optional.of()
Use Optional.of() when the value is known to be non-null.
Optional<String> name = Optional.of("Tanmoy");Passing null to Optional.of() throws NullPointerException.
Optional.ofNullable()
Use ofNullable() when the value may be null.
String name = null;
Optional<String> result = Optional.ofNullable(name);The resulting Optional is empty.
Optional.empty()
You can explicitly create an empty Optional:
Optional<String> result = Optional.empty();Checking Optional with isPresent()
Optional<String> name = Optional.of("Tanmoy");
if (name.isPresent()) {
System.out.println(name.get());
}Although isPresent() is useful, code that repeatedly checks isPresent() and then calls get() can become similar to manual null checking.
For simple conditional actions, ifPresent() is often clearer.
ifPresent()
Optional<String> name = Optional.of("Tanmoy");
name.ifPresent( value -> System.out.println(value));This executes the lambda only when the value is present.
orElse()
orElse() provides a fallback value.
Optional<String> name = Optional.empty();
String result = name.orElse("Unknown");
System.out.println(result);Output:
UnknownorElseGet()
orElseGet() accepts a Supplier and can be useful when creating the fallback value requires computation.
String result = name.orElseGet(() -> "Unknown");One important distinction is that orElse() evaluates its argument even when the Optional contains a value, while orElseGet() invokes its supplier only when a fallback is needed.
orElseThrow()
When the absence of a value should be treated as an error:
String result = name.orElseThrow(() -> new IllegalArgumentException( "Name not found"));This makes the failure condition explicit.
Optional with Streams
Optional and streams often work well together.
For example:
List<String> names = List.of("Amit", "Tanmoy", "Riya");
Optional<String> result = names.stream() .filter(name -> name.startsWith("T")) .findFirst();The result might contain:
Tanmoyor it might be empty if no matching element exists.
You can process it:
result.ifPresent( name -> System.out.println(name));Combining Lambda, Functional Interface, Stream, and Optional
These Java 8 features become particularly powerful when used together.
Consider:
List<String> names = List.of( "Amit", "Tanmoy", "Riya", "Thomas");
Optional<String> result = names.stream() .filter(name -> name.length() > 5) .map(String::toUpperCase) .findFirst();
result.ifPresent( name -> System.out.println(name));The pipeline works like this:
List↓stream()↓filter()↓map()↓findFirst()↓Optional↓ifPresent()This is one of the most important patterns introduced with Java 8.
Method References
Java 8 also introduced method references, which provide a compact alternative to certain lambda expressions.
Instead of:
names.forEach( name -> System.out.println(name));you can write:
names.forEach(System.out::println);Another example:
names.stream() .map(name -> name.toUpperCase())can become:
names.stream() .map(String::toUpperCase)Method references are not a separate replacement for functional interfaces. They are another concise way to provide behavior where a functional interface is expected.
Lambda vs Anonymous Class
Before Java 8:
Comparator<String> comparator = new Comparator<String>() {
@Override public int compare( String a, String b) {
return a.length() - b.length();
}
};With a lambda:
Comparator<String> comparator =(a, b) -> a.length() - b.length();The lambda version is shorter and focuses on the behavior rather than the surrounding boilerplate.
Stream vs Collection
A collection stores data.
A stream processes data.
For example:
Collection↓Stores elementswhile:
Stream↓Processes elementsA stream does not normally modify the source collection simply because you perform stream operations on it.
For example:
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> doubled = numbers.stream() .map(n -> n * 2) .collect(Collectors.toList());The original numbers collection remains unchanged.
Lazy Evaluation in Streams
Intermediate stream operations are generally lazy.
For example:
numbers.stream() .filter(n -> {
System.out.println("Checking " + n);
return n > 3;
}) .findFirst();Because findFirst() can stop the pipeline once it finds a matching element, not every source element necessarily needs to be processed.
This behavior is one reason stream pipelines can be expressive and efficient when used appropriately.
Sequential vs Parallel Streams
A normal stream is sequential:
numbers.stream()A parallel stream can process operations using multiple threads:
numbers.parallelStream()Parallel streams should not automatically be assumed to be faster.
They can be useful for suitable workloads, but overhead, workload size, ordering requirements, shared state, and the cost of each operation all matter.
For many ordinary collection operations, a sequential stream is simpler and sufficient.
Common Java 8 Stream Mistakes
Modifying Shared State
Avoid code like:
List<Integer> result = new ArrayList<>();
numbers.stream() .forEach(n -> result.add(n * 2));Prefer:
List<Integer> result = numbers.stream() .map(n -> n * 2) .collect(Collectors.toList());This expresses the transformation directly and avoids unnecessary shared mutable state.
Using Streams for Everything
Streams are useful, but they are not automatically better than loops.
A normal loop can sometimes be clearer, especially for complex control flow, stateful algorithms, or code that needs multiple related operations.
Choose the approach that makes the logic easiest to understand and maintain.
Common Optional Mistakes
Calling get() Without Checking
This can fail:
Optional<String> name = Optional.empty();
System.out.println(name.get());It throws:
NoSuchElementExceptionPrefer:
name.ifPresent(System.out::println);or provide an appropriate fallback.
Using Optional as Every Field
Optional is primarily designed to represent potentially absent results and return values.
It is not automatically the best choice for every field, method parameter, or data structure.
Use it where it communicates absence clearly.
Java 8 Feature Comparison
| Feature | Main Purpose |
|---|---|
| Lambda Expression | Write behavior concisely |
| Functional Interface | Define a single abstract operation |
| Stream API | Process sequences of data |
| Optional | Represent an optional result |
| Method Reference | Shorten certain lambdas |
| Default Method | Add implementation to an interface |
CompletableFuture | Compose asynchronous computations |
Real-World Example
Imagine an application containing employees:
class Employee {
private String name;
private String department;
private double salary;
// constructor and getters
}Suppose we want to find the first employee from the IT department earning more than ₹50,000.
A Java 8 solution could look like:
Optional<Employee> employee = employees.stream() .filter(e -> e.getDepartment().equals("IT")) .filter(e -> e.getSalary() > 50000) .findFirst();Then:
employee.ifPresent(e -> System.out.println(e.getName()));This example combines:
Lambda expressions +Functional interfaces +Streams +OptionalThese four concepts are therefore not isolated features. They are designed to work together.
Java 8 Features and Functional Programming
Java remains an object-oriented language, but Java 8 introduced several functional programming concepts.
Important ideas include:
Treating behavior as values through functional interfaces
Passing behavior using lambdas
Transforming data with streams
Avoiding some explicit null handling with
OptionalUsing immutable-style operations where practical
Composing operations into pipelines
This makes Java code more expressive without requiring developers to abandon object-oriented design.
Frequently Asked Questions
What are the main Java 8 features?
Important Java 8 additions include lambda expressions, functional interfaces, the Stream API, Optional, method references, default and static interface methods, the new Date and Time API, and CompletableFuture.
What is a lambda expression?
A lambda expression is a concise way to provide an implementation for a functional interface.
Example:
(a, b) -> a + bWhat is a functional interface?
A functional interface has exactly one abstract method. It can be used as the target type of a lambda expression or method reference.
What is the Stream API?
The Stream API provides a declarative way to process sequences of data using operations such as filter, map, sorted, collect, and reduce.
Is a Stream a Collection?
No. A collection stores data, while a stream represents a pipeline for processing data.
What is Optional in Java?
Optional<T> represents a value that may be present or absent.
Does Optional eliminate NullPointerException?
No. Optional does not make null-related errors impossible. It provides an explicit API for representing and handling absence in appropriate situations.
What is the difference between map() and filter()?
map() transforms each element into another value, while filter() keeps only elements that satisfy a condition.
What is the difference between orElse() and orElseGet()?
orElse() receives a fallback value that is evaluated before the method call. orElseGet() receives a supplier and invokes it only when the Optional is empty.
Are parallel streams always faster?
No. Parallel processing has overhead and is beneficial only for suitable workloads.
Suggested Graphics
Graphic 1 — Java 8 Feature Overview
JAVA 8 | +------------+------------+ | | | Lambda Streams Optional | Functional InterfacesGraphic 2 — Stream Pipeline
Collection↓stream()↓filter()↓map()↓sorted()↓collect()↓ResultGraphic 3 — Optional Flow
Value | ofNullable() | +-----+-----+ | |Present Empty | |ifPresent orElse | |Result DefaultGraphic 4 — Four Core Java 8 Concepts
Lambda↓Provides behaviorFunctional Interface↓Defines target typeStream↓Processes dataOptional↓Represents possible absenceRunnable Code Example: Java 8 Features
Java 17+import java.util.List;
import java.util.Optional;
public class Java8Demo {
public static void main(String[] args) {
List<String> names = List.of("Anna", "Bob", "Alexander", "Charlie");
// Streams & Lambdas
names.stream() .filter(name -> name.length() > 3) .map(String::toUpperCase) .forEach(System.out::println);
// Optional
Optional<String> firstLongName = names.stream() .filter(n -> n.startsWith("A")) .findFirst();
System.out.println("Found: " + firstLongName.orElse("None"));
}
}