Topic 101 of 119 13. Java New Features Interactive Tutorial

Java 8 Features: Lambda Expressions, Streams, Functional Interfaces, and Optional

9 mins read Last updated: 22 Aug 2026 32,473 views

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

  • Optional

  • Method references

  • Default methods in interfaces

  • Static methods in interfaces

  • New Date and Time API

  • Improved forEach() support

  • CompletableFuture

The four features covered here are particularly important because they are commonly used together.

Main.java
Run Online
Lambda Expressions↓Functional Interfaces↓Stream API↓Optional

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:

Main.java
Run Online
Runnable task = new Runnable() {
    @Override public void run() {
        System.out.println("Task running");
    }
};

With a lambda expression:

Main.java
Run Online
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:

Main.java
Run Online
(parameters) -> expression

or:

Main.java
Run Online
(parameters) -> {
    statements;
}

For example:

This lambda accepts two integers and returns their sum.


Lambda with One Parameter

When there is one parameter, parentheses can often be omitted:

Main.java
Run Online
name -> System.out.println(name)

This is equivalent to:

Main.java
Run Online
(name) -> System.out.println(name)

For example:

Main.java
Run Online
List<String> names = List.of("Amit", "Riya", "Tanmoy");
names.forEach(name -> System.out.println(name));

Output:


Lambda with Multiple Parameters

Multiple parameters require parentheses:

For example:

Main.java
Run Online
BinaryOperator<Integer> add =(a, b) -> a + b;
System.out.println(add.apply(10, 20));

Output:


2. 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:

Main.java
Run Online
@FunctionalInterfaceinterface Calculator {
    int calculate(int a, int b);
}

A lambda can provide the implementation:

Main.java
Run Online
Calculator calculator =(a, b) -> a + b;
System.out.println( calculator.calculate(10, 20));

Output:

The 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:

Main.java
Run Online
Lambda Expression↓Functional Interface↓Implementation of its abstract method

For example:

Main.java
Run Online
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.

InterfaceMain MethodPurpose
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.

Main.java
Run Online
Predicate<Integer> isEven = number -> number % 2 == 0;
System.out.println(isEven.test(10));

Output:

Predicates are commonly used with streams.


Function

A Function converts one value into another.

Main.java
Run Online
Function<String, Integer> length = text -> text.length();
System.out.println(length.apply("Java"));

Output:


Consumer

A Consumer accepts a value but does not return a result.

Main.java
Run Online
Consumer<String> printer = text -> System.out.println(text);
printer.accept("Hello Java");

Output:


Supplier

A Supplier produces a value without taking an input.

Main.java
Run Online
Supplier<String> message =() -> "Hello Java";
System.out.println(message.get());

Output:


3. 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:

Main.java
Run Online
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

You can create a stream:

and perform operations on it.


Basic Stream Example

Suppose we want only even numbers.

Traditional loop:

Main.java
Run Online
for (Integer number : numbers) {
    if (number % 2 == 0) {
        System.out.println(number);
    }
}

Using streams:

Main.java
Run Online
numbers.stream() .filter(number -> number % 2 == 0) .forEach(System.out::println);

Output:

The stream version describes the operation more directly:

Main.java
Run Online
numbers↓filter even numbers↓print result

Stream Pipeline

A stream pipeline generally consists of:

Main.java
Run Online
Source↓Intermediate Operations↓Terminal Operation

Example:

Main.java
Run Online
numbers.stream() .filter(n -> n > 3) .map(n -> n * 2) .forEach(System.out::println);

Here:

  • numbers is the source

  • filter() is an intermediate operation

  • map() is an intermediate operation

  • forEach() is a terminal operation


Intermediate Operations

Intermediate operations transform or filter a stream and return another stream.

Common examples include:

Main.java
Run Online
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.

Main.java
Run Online
List<Integer> numbers = List.of(10, 15, 20, 25, 30);
numbers.stream() .filter(n -> n >= 20) .forEach(System.out::println);

Output:


map()

map() transforms each element.

Main.java
Run Online
List<String> names = List.of("java", "spring", "mysql");
names.stream() .map(String::toUpperCase) .forEach(System.out::println);

Output:


sorted()

sorted() sorts stream elements according to their natural ordering or a supplied comparator.

Main.java
Run Online
List<Integer> numbers = List.of(50, 10, 40, 20, 30);
numbers.stream() .sorted() .forEach(System.out::println);

Output:


distinct()

distinct() removes duplicate elements according to their equality semantics.

Main.java
Run Online
List<Integer> numbers = List.of(10, 20, 10, 30, 20);
numbers.stream() .distinct() .forEach(System.out::println);

Output:


limit()

limit() restricts the number of elements.

Main.java
Run Online
List<Integer> numbers = List.of(10, 20, 30, 40, 50);
numbers.stream() .limit(3) .forEach(System.out::println);

Output:


Terminal Operations

A terminal operation produces a final result or side effect and causes the stream pipeline to execute.

Common terminal operations include:

Main.java
Run Online
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.

Main.java
Run Online
List<String> result = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList());

For modern Java versions, you may also encounter:

Main.java
Run Online
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.

Main.java
Run Online
long count = numbers.stream() .filter(n -> n > 20) .count();
System.out.println(count);

reduce()

reduce() combines stream elements into a single result.

For example:

Main.java
Run Online
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:

A method reference can make this shorter:

Main.java
Run Online
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:

Main.java
Run Online
String name = findName();
if (name != null) {
    System.out.println(name);
}

you can represent the result as:

Main.java
Run Online
Optional<String> name = findName();

The important idea is that Optional represents:

Main.java
Run Online
Value exists ORNo value exists

Creating Optional Objects

Optional.of()

Use Optional.of() when the value is known to be non-null.

Main.java
Run Online
Optional<String> name = Optional.of("Tanmoy");

Passing null to Optional.of() throws NullPointerException.


Optional.ofNullable()

Use ofNullable() when the value may be null.

Main.java
Run Online
String name = null;
Optional<String> result = Optional.ofNullable(name);

The resulting Optional is empty.


Optional.empty()

You can explicitly create an empty Optional:

Main.java
Run Online
Optional<String> result = Optional.empty();

Checking Optional with isPresent()

Main.java
Run Online
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()

Main.java
Run Online
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.

Main.java
Run Online
Optional<String> name = Optional.empty();
String result = name.orElse("Unknown");
System.out.println(result);

Output:


orElseGet()

orElseGet() accepts a Supplier and can be useful when creating the fallback value requires computation.

Main.java
Run Online
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:

Main.java
Run Online
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:

Main.java
Run Online
List<String> names = List.of("Amit", "Tanmoy", "Riya");
Optional<String> result = names.stream() .filter(name -> name.startsWith("T")) .findFirst();

The result might contain:

or it might be empty if no matching element exists.

You can process it:

Main.java
Run Online
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:

Main.java
Run Online
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:

Main.java
Run Online
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:

Main.java
Run Online
names.forEach( name -> System.out.println(name));

you can write:

Main.java
Run Online
names.forEach(System.out::println);

Another example:

Main.java
Run Online
names.stream() .map(name -> name.toUpperCase())

can become:

Main.java
Run Online
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:

Main.java
Run Online
Comparator<String> comparator = new Comparator<String>() {
    @Override public int compare( String a, String b) {
        return a.length() - b.length();
    }
};

With a lambda:

Main.java
Run Online
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:

Main.java
Run Online
Collection↓Stores elements

while:

Main.java
Run Online
Stream↓Processes elements

A stream does not normally modify the source collection simply because you perform stream operations on it.

For example:

Main.java
Run Online
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:

Main.java
Run Online
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:

A parallel stream can process operations using multiple threads:

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:

Main.java
Run Online
List<Integer> result = new ArrayList<>();
numbers.stream() .forEach(n -> result.add(n * 2));

Prefer:

Main.java
Run Online
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:

Main.java
Run Online
Optional<String> name = Optional.empty();
System.out.println(name.get());

It throws:

Prefer:

Main.java
Run Online
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

FeatureMain Purpose
Lambda ExpressionWrite behavior concisely
Functional InterfaceDefine a single abstract operation
Stream APIProcess sequences of data
OptionalRepresent an optional result
Method ReferenceShorten certain lambdas
Default MethodAdd implementation to an interface
CompletableFutureCompose asynchronous computations

Real-World Example

Imagine an application containing employees:

Main.java
Run Online
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:

Main.java
Run Online
Optional<Employee> employee = employees.stream() .filter(e -> e.getDepartment().equals("IT")) .filter(e -> e.getSalary() > 50000) .findFirst();

Then:

Main.java
Run Online
employee.ifPresent(e -> System.out.println(e.getName()));

This example combines:

Main.java
Run Online
Lambda expressions +Functional interfaces +Streams +Optional

These 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 Optional

  • Using 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:

What 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

Main.java
Run Online
JAVA 8 | +------------+------------+ | | | Lambda Streams Optional | Functional Interfaces

Graphic 2 — Stream Pipeline

Main.java
Run Online
Collection↓stream()↓filter()↓map()↓sorted()↓collect()↓Result

Graphic 3 — Optional Flow

Main.java
Run Online
Value | ofNullable() | +-----+-----+ | |Present Empty | |ifPresent orElse | |Result Default

Graphic 4 — Four Core Java 8 Concepts

Main.java
Run Online
Lambda↓Provides behaviorFunctional Interface↓Defines target typeStream↓Processes dataOptional↓Represents possible absence

Runnable Code Example: Java 8 Features

Java 17+
Main.java
Run Online
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"));
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

What is the primary purpose of Optional in Java 8?

To provide a type-level representation of optional values and encourage explicit handling of missing values, reducing NullPointerExceptions.
Q2

What is a method reference in Java 8?

A shorthand syntax for a lambda that calls an existing method by name (e.g. String::toUpperCase).

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Use Optional strictly for method return types; never use Optional as method parameters, class fields, or collection elements.