Unlocking the power of functional programming in Java

18 April, 2024
img/functional-programming-java.jpg

Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions. It emphasizes immutability, first-class functions, and the avoidance of changing state and mutable data. In contrast to imperative programming, which focuses on changing state through the use of statements, functional programming relies on the evaluation of expressions.

Java, in it’s root, is an object-oriented programming language, but it also supports functional programming through the use of lambda expressions and functional interfaces. These are the features that have been added to Java since version 8, and have greatly improved expresiveness of the language.

In this post, we’ll go over the main concepts of Functional Programming (FP) and how they can be used in Java applications.

Table Of Contents

Introduction to Functional Programming

Functional Programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. In functional programming, functions are first-class citizens, meaning they can be passed around as arguments, returned from other functions, and assigned to variables. The paradigm emphasizes immutability, where data once created cannot be modified, and the use of pure functions that produce deterministic outputs based solely on their inputs, without side effects.

Let’s take a look at the most important concepts in Functional Programming.

  • First-class functions - in FP, functions are treated as first-class citizens, meaning they can be used in the same way as other data types. They can be assigned to variables, passed as arguments to other functions, and returned as values from other functions.
  • Immutability - it means that once data is created, it can’t be modified. Instead of modifying existing data, functional programming encourages creating new data structures with the desired changes.
  • Pure functions - a pure function is a function that, given the same input, will always produce the same output and has no observable side effects. Pure functions facilitate reasoning about the behavior of the program and make it easier to understand and test.
  • Avoidance of side-effects - FP aims to minimize or eliminate side effects, which are changes to the state of the program that are not reflected in the function’s return value.
  • Higher-order functions - FP often leverages higher-order functions, which are functions that take one or more functions as arguments or return functions as results. Higher-order functions enable the composition of functions and the creation of more abstract and reusable code.
  • Referential transparency - it means that a function’s result depends solely on its input and doesn’t rely on external state.
  • Declarative style - FP promotes a declarative style, where the focus is on expressing what the program should accomplish rather than explicitly detailing how to achieve it. This can lead to more concise and expressive code.

Functional programming languages, such as Haskell, Scala, and Clojure, are designed with these principles in mind from the ground up. Unfortunatelly, it is not the case with Java, which was initially designed as purely Object Oriented language. But, with FP features added from Java 8 on, most of these concepts are covered in Java.

Functional Programming in Java

In this section of the post, we’ll take a look at how FP concepts are implemented in Java, and how we can leverage them in Java applications.

Functional interfaces

In Java, a functional interface is an interface that has a single abstract method. This means that the interface can only have one method that does not have a default implementation. Functional interfaces are often used in conjunction with lambda expressions, which are anonymous functions that can be passed around as objects.

In addition to having a single abstract method, functional interfaces are usually annotated with @FunctionalInterface annotation. This is an information annotation that conveys the intention to use the interfcae as functional.

An example of functional interface:

@FunctionalInterface
public interface MyFunctionalInterface {
  void doSomething();
}

This functional interface has a single abstract method, doSomething(), which does not have a default implementation. This means that any class that implements this interface must provide its own implementation of the doSomething() method. Functional interfaces are commonly used in lambda expressions, so it can be used like this:

public void someMethod(MyFunctionalInterface mfi) {
  mfi.do Something();
} 

// call someMethod with lambda expression
someMethod(() -> System.out.println("called doSomething() method"));

In lambda expression, Java runtime calls the method of functional interface under the hood.

First class functions in Java

When programming language is said to support first-class functions, it means that functions have the following features:

  • can be assigned to variables
Function<Integer, Integer> square = x -> x * x;
  • can be passed as arguments
Function<Integer, Integer> square = x -> x * x;

// Higher-order function taking a function as an argument
int result = applyOperation(5, square);
  • can be returned from methods
Function<Integer, Function<Integer, Integer>> multiplier = x -> y -> x * y;
  • can be stored in data strucrtures
List<Function<Integer, Integer>> functionList = new ArrayList<>();
functionList.add(x -> x * x);
functionList.add(x -> x + 1);

This means that Java also supports higher order functions, ie. functions that return a function, or take other functions as an argument.

Function interface

Interface Function represents a function that takes one argument and returns a result. It defines single abstract method apply(), which performs the calculation.

For example, this implementation multiplies an argument by two:

Function<Integer, Integer> multiplyByTwo = x -> 2 * x;
System.out.println("result: " + multiplyByTwo.apply(3)); // prints 6

In addition to apply(), Function interfaces provides two more default methods:

  • default <V> Function<T,V> andThen(Function<? super R,? extends V> after) - this method returns composed function which first applies this function to it’s input and then applies after function to the output
  • default <V> Function<V,R> compose(Function<? super V,? extends T> before) - it returns composed function that first applies before function to it’s input and then applies this function to the result.

An example of using these methods:

 // Define two functions: one that squares a number and one that adds 1
Function<Integer, Integer> square = (n) -> n * n;
Function<Integer, Integer> addOne = (n) -> n + 1;

// Combine the two functions using andThen()
Function<Integer, Integer> addOneThenSquare = square.andThen(addOne);

// Call the combined function and print the result
System.out.println(addOneThenSquare.apply(2));  // prints 5

Function<Integer, Integer> squareThenAddOne = square.compose(addOne);

// Call the combined function and print the result
System.out.println(squareThenAddOne.apply(2));  // prints 9

As you can see in the example, the order of executions differs and affects the result of the computation.

Predicate function

Predicate is a function which takes one argument and returns a boolean value. It contains one abstract method test(T t) which takes an argument of type T and returns a boolean value.

 Predicate<String> isEmpty = (s) -> s.isEmpty();
    
// Test the Predicate and print the result
System.out.println(isEmpty.test(""));  // prints true
System.out.println(isEmpty.test("hello"));  // prints false

In addition to test() mthod, Predicate also contains a couple of usefule default methods:

  • default Predicate<T> and(Predicate<? super T> other) - Returns a composed predicate that represents a short-circuiting logical AND of this predicate and another.
  • default Predicate<T> negate() - Returns a predicate that represents the logical negation of this predicate.
  • default Predicate<T> or(Predicate<? super T> other) - Returns a composed predicate that represents a short-circuiting logical OR of this predicate and another.

Conclusion

Although functional programming is not Java’s forte, the support for it in a language is quite good. It allows developers to write concise and easy to understand code.

If you have any questions or comments about the post, please don’t hesitate to write a comment.