Functional Programming in Java

Haresh Akalanka
Nerd For Tech
Published in
8 min readMar 2, 2023
Photo by Michiel Leunens on Unsplash

Functional programming is a programming paradigm that focuses on the use of functions to accomplish tasks, as opposed to traditional imperative programming which relies on statements that change the program's state. In recent years, functional programming has gained a lot of popularity, especially with the rise of modern programming languages like Haskell, Scala, and Kotlin. But what many people don’t know is that functional programming is also possible in Java, one of the most popular programming languages in the world. In this blog post, we will explore how to use functional programming techniques in Java to write more expressive and efficient code.

One of the key concepts in functional programming is the use of pure functions. A pure function is a function that, given the same input, will always return the same output, without causing any side effects. In contrast, an impure function can have side effects, such as modifying a global variable or interacting with an external system. Pure functions are more predictable and easier to test, which is why functional programming emphasizes their use.

Java has a built-in functional interface called Function<T, R>, which can be used to represent a pure function that takes an input of type T and returns an output of the type R. This interface can be used to create a lambda expression, which defines a function without creating a separate class. For example, the following code defines a lambda expression that takes an integer and returns its square:

Function<Integer, Integer> square = x -> x * x;

We can then use this function to square any number:

int result = square.apply(5); // 25

Another important concept in functional programming is the use of higher-order functions. A higher-order function is a function that takes one or more functions as arguments, and/or returns a function as its result. In Java, we can use the Function<T, R> interface to create higher-order functions, as well as other functional interfaces such as Predicate<T> (a function that returns a boolean) and Consumer<T> (a function that takes an input but doesn’t return anything).

For example, we can create a higher-order function that takes a function and applies it to a list of elements:

public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for (T x : list) {
result.add(f.apply(x));
}
return result;
}

This function can then be used to apply the square function we defined earlier to a list of integers:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = map(numbers, square); // [1, 4, 9, 16, 25]

Another way to use functional programming in Java is to use the Stream API, which is a built-in library for performing operations on collections of elements. The Stream API is based on the concepts of functional programming and allows you to perform operations such as filtering, mapping, and reducing on a stream of elements.

For example, we can use the Stream API to filter a list of numbers and find the first even number:

Integer firstEven = numbers.stream()
.filter(x -> x % 2 == 0)
.findFirst()
.orElse(null);

In addition to the Stream API, Java also has the Optional class, which is a way to represent the possibility of a null value. An Optional<T> object can either contain a value of type T or be empty. This can be useful for avoiding null pointer exceptions and making the code more explicit about the possibility of missing values.

For example, we can use the Optional class to make the previous example more robust:

Optional<Integer> firstEven = numbers.stream()
.filter(x -> x % 2 == 0)
.findFirst();

if (firstEven.isPresent()) {
System.out.println(firstEven.get());
} else {
System.out.println("No even numbers found.");
}

Another important concept in functional programming is immutability. In functional programming, it is considered best practice to avoid modifying the state of objects and instead create new objects with updated values. This makes the code more predictable and easier to reason about.

In Java, we can use the final keyword to make an object immutable and use the builder pattern to create new objects with updated values. For example, we can create an immutable class for a person:

public final class Person {
private final String name;
private final int age;

private Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

public static class Builder {
private String name;
private int age;

public Builder setName(String name) {
this.name = name;
return this;
}

public Builder setAge(int age) {
this.age = age;
return this;
}

public Person build() {
return new Person(name, age);
}
}
}

This class can be used to create new Person objects with updated values:

Person person = new Person.Builder()
.setName("John")
.setAge(30)
.build();

Another powerful feature of functional programming is the ability to compose functions. Function composition is the process of combining two or more functions to create a new function. This allows you to create complex functions by combining simple building blocks. In Java, you can use the compose() and andThen() methods of the Function<T, R> interface to compose functions.

For example, we can create a function that takes an integer and returns its square and then create another function that takes an integer and returns its square root. We can then compose these two functions to create a new function that takes an integer, squares it, and then finds the square root:

Function<Integer, Double> square = x -> x * x;
Function<Double, Double> sqrt = x -> Math.sqrt(x);
Function<Integer, Double> sqrtOfSquare = sqrt.compose(square);

We can also use the andThen() method to chain functions together in a different order:

Function<Integer, Double> squareOfSqrt = square.andThen(sqrt);

Functional programming also encourages the use of recursion, which is a way of defining a function that calls itself. Recursion can be used to solve problems that can be broken down into smaller subproblems, such as finding the factorial of a number or traversing a tree. In Java, you can use recursion to implement functional programming techniques, but you have to be careful to ensure that the function terminates and that the function is tail-recursive.

public static void main(String[] args) {
int n = 5;
int result = factorial.apply(n);
System.out.println("Factorial of " + n + " is " + result);
}

static Function<Integer, Integer> factorial = n -> n == 0 ? 1 : n * factorial.apply(n-1);

Functional programming also encourages the use of pattern matching, which is a way of matching the structure of an object to determine its behavior. In functional programming languages like Haskell, pattern matching is a built-in feature, but in Java, you can use the Visitor pattern to implement pattern matching.

Another important aspect of functional programming is the use of currying, which is the process of converting a function that takes multiple arguments into a chain of functions that each takes a single argument. This allows you to create more reusable and composable functions. In Java, you can use lambda expressions and functional interfaces to implement currying.

For example, we can create a function that takes two integers and returns their sum:

BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;

We can then use currying to convert this function into a series of functions that each take a single argument:

Function<Integer, Function<Integer, Integer>> addCurried = x -> y -> x + y;

This curried function can then be used to partially apply one of the arguments and create a new function that takes the remaining argument:

Function<Integer, Integer> addFive = addCurried.apply(5);

This new function can then be used to add five to any integer:

int result = addFive.apply(3); // 8

Functional programming also encourages the use of monads, which are a way of representing computations that may have side effects, such as IO or error handling. In Java, you can use the Optional and Either monad to represent computations that may have missing or error values.

Another important aspect of functional programming is the use of lazy evaluation, which is a way of delaying the evaluation of an expression until its value is needed. This can be useful for improving performance and reducing memory usage, especially when working with large collections of data. In Java, you can use the Stream API to achieve lazy evaluation by using intermediate operations such as filter and map, which return a new stream that can be further processed.

For example, we can use the Stream API to filter a list of numbers and find the first even number, without having to iterate over the entire list:

Optional<Integer> firstEven = numbers.stream()
.filter(x -> x % 2 == 0)
.findFirst();

The filter operation returns a new stream that contains only the even numbers, and the findFirst operation returns the first element of the stream, or an empty Optional if the stream is empty. This means that the filter and findFirst operations are only executed for the first element of the stream, and the rest of the elements are never evaluated.

Another way to achieve lazy evaluation in Java is to use the Supplier<T> interface, which is a functional interface that represents a supplier of results. A supplier is a function that takes no arguments and returns a value of type T. In Java, you can use the Supplier<T> interface to create lazily evaluated expressions.

For example, we can use a supplier to create a lazily evaluated expression that calculates the factorial of a number:

Supplier<BigInteger> factorial = new Supplier<BigInteger>() {
private int n = 1;
private BigInteger result = BigInteger.ONE;

@Override
public BigInteger get() {
if (n == 1) {
return result;
}
result = result.multiply(BigInteger.valueOf(n));
n++;
return get();
}
};

This supplier can then be used to calculate the factorial of a number without having to calculate all the previous factorials.

Another important aspect of functional programming is the use of type inference, which is a way of automatically deducing the types of variables and expressions based on their usage. Type inference can make the code more concise and readable, as well as reduce the need for explicit type annotations. Java supports type inference through the use of the var keyword and types inference in lambda expressions.

For example, we can use the var keyword to declare a variable without specifying its type:

var numbers = Arrays.asList(1, 2, 3, 4, 5);

The compiler will infer the type of the variable based on the type of the expression on the right-hand side, in this case, a List<Integer>.

We can also use type inference in lambda expressions to make the code more concise and readable. For example, the following code defines a lambda expression that takes an integer and returns its square:

Function<Integer, Integer> square = x -> x * x;

But we can use type inference to remove the explicit type annotations:

var square = (Integer x) -> x * x;

Type inference can also be used in the context of the Stream API to make the code more readable and concise.

List<Integer> result = numbers.stream()
.filter(x -> x % 2 == 0)
.map(x -> x * x)
.collect(Collectors.toList());

In conclusion, functional programming is a powerful programming paradigm that can be used to write more expressive and efficient code. Java is a language that supports functional programming through its built-in functional interfaces, the Stream API, and the Optional class. By using functional programming techniques such as pure functions, higher-order functions, immutability, recursion, pattern matching, currying, monads, lazy evaluation, and type inference, you can make your code more predictable, easier to test, and easier to reason about. With functional programming concepts, you can write more elegant and maintainable code in Java. These concepts also help to improve performance, reduce memory usage and make the code more readable and concise.

Cheers…!

--

--

Haresh Akalanka
Nerd For Tech

Undergraduate in university of Moratuwa, Sri Lanka.