Functional Programming (FP) in Java became mainstream after Java 8 and transformed how we write collections processing, asynchronous workflows, APIs, and clean business logic.
This guide covers:
- Functional Programming basics
- Functional Interfaces
- Lambda Expressions
- Method References
- Stream API
- Optional
- Built-in Functional Interfaces
- Functional Composition
- Parallel Streams
- Best Practices
- Performance Considerations
- Interview Questions
- Real-world backend examples
1. What is Functional Programming?
Functional Programming is a programming paradigm where:
- Functions are treated as first-class citizens
- Data is immutable whenever possible
- Side effects are minimized
- Declarative style is preferred over imperative style
Instead of:
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.toUpperCase());
}
}
We write:
List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.toList();
2. Why Functional Programming?
Advantages
- Cleaner code
- Less boilerplate
- Easier parallelism
- Better readability
- Easier testing
- Declarative programming
- Better collection processing
3. Functional Interface
A Functional Interface contains exactly one abstract method.
Java provides:
@FunctionalInterface
annotation.
Example
@FunctionalInterface
interface Calculator {
int operate(int a, int b);
}
Usage:
public class Main {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
System.out.println(add.operate(10, 20));
}
}
Output:
30
4. Lambda Expressions
Lambda expression provides implementation of functional interfaces.
Syntax
(parameters) -> expression
or
(parameters) -> {
statements
}
Examples
No Parameter
Runnable task = () -> System.out.println("Running");
Single Parameter
Consumer<String> printer = name -> System.out.println(name);
Multiple Parameters
BinaryOperator<Integer> sum = (a, b) -> a + b;
5. Built-in Functional Interfaces
Package:
java.util.function
5.1 Predicate
Used for conditions.
Predicate<Integer> isEven = num -> num % 2 == 0;
System.out.println(isEven.test(10));
Output:
true
Combining Predicates
Predicate<Integer> greaterThan10 = n -> n > 10;
Predicate<Integer> even = n -> n % 2 == 0;
Predicate<Integer> combined =
greaterThan10.and(even);
System.out.println(combined.test(20));
5.2 Function
Transforms input to output.
Function<String, Integer> length = str -> str.length();
System.out.println(length.apply("Java"));
Function Chaining
Function<Integer, Integer> multiply = x -> x * 2;
Function<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> result =
multiply.andThen(square);
System.out.println(result.apply(5));
Steps:
5 * 2 = 10
10 * 10 = 100
5.3 Consumer
Consumes input without returning value.
Consumer<String> print = msg -> System.out.println(msg);
print.accept("Hello");
5.4 Supplier
Produces value.
Supplier<Double> random = () -> Math.random();
System.out.println(random.get());
5.5 UnaryOperator
Input and output same type.
UnaryOperator<Integer> square = x -> x * x;
System.out.println(square.apply(5));
5.6 BinaryOperator
Two inputs same type → one output same type.
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println(add.apply(10, 20));
6. Method References
Cleaner alternative to lambda.
Types
| Type | Example |
|---|---|
| Static Method | Math::abs |
| Instance Method | String::toUpperCase |
| Constructor | ArrayList::new |
Example
List<String> names = List.of("john", "alice");
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
7. Stream API
Stream API is the heart of Functional Programming in Java.
Streams process data declaratively.
Stream Pipeline
Source → Intermediate Operations → Terminal Operation
Example
List<Integer> nums = List.of(1,2,3,4,5);
nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.forEach(System.out::println);
Output:
4
16
8. Intermediate Operations
These are lazy operations.
filter()
List<String> names = List.of("John", "Adam", "Alex");
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
map()
Transforms data.
List<String> names = List.of("john", "alex");
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
sorted()
List<Integer> nums = List.of(5,1,9,2);
nums.stream()
.sorted()
.forEach(System.out::println);
distinct()
List<Integer> nums = List.of(1,1,2,2,3);
nums.stream()
.distinct()
.forEach(System.out::println);
limit()
Stream.iterate(1, n -> n + 1)
.limit(5)
.forEach(System.out::println);
skip()
List<Integer> nums = List.of(1,2,3,4,5);
nums.stream()
.skip(2)
.forEach(System.out::println);
flatMap()
Very important for nested collections.
List<List<String>> data = List.of(
List.of("A", "B"),
List.of("C", "D")
);
data.stream()
.flatMap(List::stream)
.forEach(System.out::println);
Output:
A
B
C
D
9. Terminal Operations
collect()
List<String> result = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
reduce()
Aggregation.
List<Integer> nums = List.of(1,2,3,4);
int sum = nums.stream()
.reduce(0, Integer::sum);
System.out.println(sum);
count()
long count = nums.stream()
.filter(n -> n % 2 == 0)
.count();
anyMatch()
boolean exists = nums.stream()
.anyMatch(n -> n > 10);
allMatch()
boolean allEven = nums.stream()
.allMatch(n -> n % 2 == 0);
findFirst()
Optional<Integer> first =
nums.stream().findFirst();
10. Collectors
groupingBy()
Map<String, List<String>> grouped =
names.stream()
.collect(Collectors.groupingBy(
name -> name.substring(0,1)
));
counting()
Map<String, Long> countMap =
names.stream()
.collect(Collectors.groupingBy(
name -> name,
Collectors.counting()
));
partitioningBy()
Map<Boolean, List<Integer>> result =
nums.stream()
.collect(Collectors.partitioningBy(
n -> n % 2 == 0
));
joining()
String joined = names.stream()
.collect(Collectors.joining(", "));
11. Optional
Optional avoids NullPointerException.
Creating Optional
Optional<String> name =
Optional.of("Java");
Optional<String> empty =
Optional.empty();
Common Methods
isPresent()
if(name.isPresent()) {
System.out.println(name.get());
}
orElse()
String value =
name.orElse("Default");
orElseGet()
String value =
name.orElseGet(() -> "Generated");
map()
Optional<String> upper =
name.map(String::toUpperCase);
Best Practice
Avoid:
if(optional.isPresent()) {
return optional.get();
}
Prefer:
return optional.orElse("default");
12. Stream Lazy Evaluation
Intermediate operations execute only when terminal operation is called.
Example
Stream.of(1,2,3)
.filter(n -> {
System.out.println("Filtering");
return n > 1;
});
Nothing executes because no terminal operation exists.
Add Terminal Operation
Stream.of(1,2,3)
.filter(n -> {
System.out.println("Filtering");
return n > 1;
})
.count();
Now execution happens.
13. Parallel Streams
Used for parallel data processing.
Example
List<Integer> nums =
IntStream.rangeClosed(1, 100)
.boxed()
.toList();
nums.parallelStream()
.map(n -> n * 2)
.forEach(System.out::println);
When to Use Parallel Streams
Use when:
- CPU intensive tasks
- Large datasets
- Stateless operations
- Independent computations
Avoid when:
- Shared mutable state
- IO operations
- Small datasets
14. Functional Composition
Combining functions together.
Example
Function<Integer, Integer> multiply = x -> x * 2;
Function<Integer, Integer> add = x -> x + 3;
Function<Integer, Integer> combined =
multiply.andThen(add);
System.out.println(combined.apply(5));
Output:
13
compose()
Function<Integer, Integer> composed =
multiply.compose(add);
System.out.println(composed.apply(5));
Execution:
(5 + 3) * 2 = 16
15. Real Backend Examples
Filtering Employees
class Employee {
String name;
int salary;
public Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
public int getSalary() {
return salary;
}
public String getName() {
return name;
}
}
List<Employee> employees = List.of(
new Employee("John", 50000),
new Employee("Alex", 90000),
new Employee("David", 70000)
);
List<String> highPaid =
employees.stream()
.filter(e -> e.getSalary() > 60000)
.map(Employee::getName)
.toList();
System.out.println(highPaid);
Grouping Transactions
Map<String, List<Transaction>> grouped =
transactions.stream()
.collect(Collectors.groupingBy(
Transaction::getStatus
));
Counting Frequencies
Map<String, Long> freq =
words.stream()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
16. Common Stream Interview Problems
Find Duplicate Elements
Set<Integer> seen = new HashSet<>();
List<Integer> duplicates =
nums.stream()
.filter(n -> !seen.add(n))
.toList();
Frequency Map
Map<String, Long> map =
names.stream()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
Find Second Highest Number
int secondHighest =
nums.stream()
.distinct()
.sorted(Comparator.reverseOrder())
.skip(1)
.findFirst()
.orElseThrow();
Convert List to Map
Map<Integer, String> map =
employees.stream()
.collect(Collectors.toMap(
Employee::getId,
Employee::getName
));
17. Common Mistakes
Using Streams for Everything
Bad:
stream.forEach(list::add);
Prefer direct collection.
Shared Mutable State
Bad:
List<Integer> result = new ArrayList<>();
nums.parallelStream()
.forEach(result::add);
Unsafe.
Reusing Streams
Bad:
Stream<Integer> stream = nums.stream();
stream.count();
stream.forEach(System.out::println);
Throws exception.
18. Performance Considerations
Streams vs Loops
Loops may be faster for:
- Tight CPU loops
- Primitive-heavy operations
Streams are better for:
- Readability
- Declarative logic
- Complex transformations
- Parallelism
Primitive Streams
Avoid boxing overhead.
Use:
IntStream
LongStream
DoubleStream
Example:
int sum = IntStream.rangeClosed(1, 100)
.sum();
19. Best Practices
Prefer Immutability
Good:
List<String> result =
names.stream().toList();
Keep Lambdas Small
Bad:
.filter(x -> {
// 30 lines
})
Extract methods instead.
Use Method References
Prefer:
String::toUpperCase
over:
s -> s.toUpperCase()
Avoid Side Effects
Bad:
stream.forEach(db::save);
20. Advanced Stream Operations
teeing()
Java 12+
var result = nums.stream()
.collect(Collectors.teeing(
Collectors.minBy(Integer::compare),
Collectors.maxBy(Integer::compare),
(min, max) -> Map.of(
"min", min.get(),
"max", max.get()
)
));
takeWhile()
List<Integer> result =
List.of(1,2,3,4,1,2)
.stream()
.takeWhile(n -> n < 4)
.toList();
dropWhile()
List<Integer> result =
List.of(1,2,3,4,1,2)
.stream()
.dropWhile(n -> n < 4)
.toList();
21. Functional Programming + CompletableFuture
Very powerful in backend systems.
CompletableFuture.supplyAsync(() -> fetchUser())
.thenApply(User::getName)
.thenAccept(System.out::println);
22. Functional Style Error Handling
Traditional
try {
process();
} catch (Exception e) {
handle(e);
}
Functional Style
Optional.ofNullable(data)
.map(this::transform)
.ifPresent(System.out::println);
23. Important Interview Questions
Theory Questions
- What is Functional Programming?
- What is a Functional Interface?
- Difference between map and flatMap?
- Difference between intermediate and terminal operations?
- What is lazy evaluation?
- What is method reference?
- Difference between Predicate and Function?
- What is Optional?
- What are Parallel Streams?
- Difference between forEach and peek?
Coding Questions
- Find duplicates using streams
- Frequency map
- Group anagrams
- Second highest salary
- Flatten nested lists
- Partition odd/even
- Top K frequent elements
- Employee grouping
- Stream API transformations
24. Summary
Functional Programming in Java enables:
- Declarative coding
- Cleaner transformations
- Better readability
- Easier concurrency
- Powerful collection processing
Core pillars:
- Functional Interfaces
- Lambdas
- Method References
- Stream API
- Optional
- Functional Composition
Modern Java backend development heavily relies on these concepts in:
- Spring Boot
- Reactive Programming
- Microservices
- Data Pipelines
- Async Processing
- Event-driven systems
Mastering Functional Programming significantly improves code quality and interview performance.

Top comments (0)