Introduction to Functional Programming in Java
For a long time, Java was exclusively associated with object-oriented programming (OOP). However, with the release of Java 8 in 2014, the language made a powerful leap towards the functional paradigm. Lambda expressions, functional interfaces, and the Stream API fundamentally changed the approach to writing code, making it more concise, readable, and less error-prone.
Functional programming in Java is not a replacement for OOP, but a powerful addition. It allows you to write declarative code, where you describe what needs to be done, rather than how. In this article, we will break down the key concepts that will help you master the functional style in Java and show practical examples.
1. Lambda Expressions and Functional Interfaces
The foundation of functional programming in Java is lambda expressions — a compact way to implement anonymous functions. Syntactically, a lambda consists of a parameter list, an arrow (->), and a body:
(parameters) -> { body }Lambdas always work in conjunction with functional interfaces. A functional interface is an interface that contains exactly one abstract method (e.g., Runnable, Comparator, Predicate).
Practical Example: Replacing an Anonymous Class
Let's compare the old (OOP) and new (functional) approaches:
// Old way (anonymous class)Runnable taskOld = new Runnable() { @Override public void run() { System.out.println("Hello from anonymous class!"); }};
// New way (lambda)Runnable taskNew = () -> System.out.println("Hello from lambda!");
// ExecutiontaskNew.run();As you can see, the lambda reduced the code from 6 lines to 1, while retaining full functionality.
Built-in Functional Interfaces (package java.util.function)
Java provides a rich set of built-in interfaces. Here are the four main ones:
Predicate<T>— takes an object, returnsboolean. Used for filtering.Function<T, R>— takes an object of one type, returns an object of another type. Used for transformations.Consumer<T>— takes an object, returns nothing (void). Used for side effects (printing output).Supplier<T>— takes nothing, returns an object. Used for lazy data generation.
Example with Predicate:
import java.util.function.Predicate;
public class Main { public static void main(String[] args) { Predicate<Integer> isEven = n -> n % 2 == 0; System.out.println(isEven.test(10)); // true System.out.println(isEven.test(7)); // false }}2. Stream API: Working with Collections in a Functional Style
The Stream API is the gem of functional programming in Java. A stream (Stream) is a sequence of elements supporting sequential and parallel operations. The main advantage: you do not modify the source data, but create a new result.
The Stream API is built on three stages:
- Creating a stream (from a collection, array, or generator).
- Intermediate operations (filtering, sorting, transformation). Return a new Stream.
- Terminal operation (collecting the result, counting, searching). Ends the stream's work.
Example: Filtering and Transforming a List
Suppose we have a list of strings, and we want to get only those longer than 3 characters, convert them to uppercase, and sort them.
import java.u