All about Functional Programming in Java

Published on May 10, 2024 9 min read

Tags:
JavaProgramming Paradigm

Introduction to Programming Paradigm

Programming Paradigm is a style of programming. The programming paradigm can be classified into two types:

  • Imperative programming paradigm
  • Declarative programming paradigm

Imperative Programming Paradigm

Imperative programming paradigm focuses on describing the steps to achieve a certain goal. In imperative programming, you specify how to accomplish a task by giving a sequence of statements that change the program's state. It emphasizes explicit state manipulation and control flow. Examples of imperative programming languages include C, C++, Java, and Python (to some extent).

Example:

import java.util.List;
 
public class ImperativeExample {
    public static int sumList(List<Integer> numbers) {
        int total = 0;
        for (int num : numbers) {
            total += num;
        }
        return total;
    }
 
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);
        int sum = sumList(numbers);
        System.out.println("Sum using imperative approach: " + sum);
    }
}

Output:

Sum using imperative approach: 15

3 Main programming approches fall under Imperative Programming Paradigm:

  • Procedural Programming: Focuses on writing procedures or routines to perform tasks using loops and conditionals. Example: C.
  • Object-Oriented Programming (OOP): Organizes code into objects with data (attributes) and behavior (methods). Emphasizes encapsulation, inheritance, and polymorphism. Examples: Java, C++, Python.
  • Parallel Processing Approach: Breaks tasks into smaller subtasks executed simultaneously to utilize multiple processing units, like CPU cores. Examples: Libraries and frameworks in Java, C++, Python.

Declarative Programming Paradigm

The declarative programming paradigm focuses on describing what needs to be accomplished rather than how to accomplish it. In declarative programming, you specify the desired outcome or result, and the language or system determines the best way to achieve it. This paradigm abstracts away low-level implementation details and emphasizes expressing logic in a more human-readable and intuitive manner.

Example:

import java.util.List;
 
public class DeclarativeExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);
        int sum = numbers.stream().mapToInt(Integer::intValue).sum();
        System.out.println("Sum using declarative approach: " + sum);
    }
}

Output:

Sum using declarative approach: 15

Examples of declarative programming languages and paradigms include:

  • Functional Programming: Emphasizes pure functions and immutable data structures for computation. Focuses on composing functions to transform data, often using higher-order functions and recursion.
  • Logic Programming: Expresses computation as logical rules and constraints. Programs declare relationships between entities rather than specifying explicit steps for computation. Examples include Prolog and Datalog.
  • SQL (Structured Query Language): Declarative language for querying and manipulating relational databases. SQL queries declare the desired data set, allowing the database management system to determine the most efficient way to execute the query.

What is Functional Programming?

Functional programming 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 treated like any other data type. This allows functions to be passed as arguments to other functions, returned as values from other functions, and assigned to variables.

Key characteristics of functional programming include:

  • No State: Pure functional programming aims to eliminate mutable state and shared state as much as possible. Functions should not rely on or modify state external to their scope. Instead, they operate solely on their input parameters and produce output without altering any external state.
  • Pure Functions: Pure functions have two key characteristics:
    • They always produce the same output for the same input, regardless of any external factors or the number of times they're called. This property is referred to as referential transparency.
    • They do not cause any side effects. That is, they do not modify any external state or interact with the outside world (such as reading from or writing to files, databases, or the network).
  • No Side Effects: As mentioned, pure functions don't have side effects. This principle ensures that calling a function doesn't alter the state of the program or the environment in any way other than returning a value.
  • Higher Order Functions (HOF): Higher order functions are functions that either take other functions as arguments, return functions as results, or both. They allow for the abstraction and composition of behavior, enabling concise and expressive code.

Functional programming in Java

Functional programming in Java involves leveraging features introduced in Java 8 and later, primarily the Stream API and lambda expressions. Here's a brief overview of functional programming concepts in Java:

  • Functional Interfaces: Functional interfaces are interfaces with a single abstract method. They can be implemented using lambda expressions or method references. Examples include Predicate, Function, Consumer, and Supplier.
  • Lambda Expressions: Lambda expressions allow you to concisely express instances of single-method interfaces (functional interfaces) using a more compact syntax. This enables passing behavior as arguments to methods, enabling functional programming paradigms.
  • Stream API: The Stream API provides a fluent and functional approach to processing collections of objects. It allows you to perform operations like filter, map, reduce, and collect on streams of elements. Streams promote a declarative style of programming, where you specify what operations to perform rather than how to perform them.
  • Optional: Optional is a container object that may or may not contain a non-null value. It encourages handling of potentially absent values in a functional style, reducing the need for null checks.
  • Method References: Method references provide a way to refer to methods or constructors without invoking them. They can be used to make lambda expressions more concise by referring to existing methods directly.

Let's delve into each of the key functional programming features in Java in more detail:

1. Functional Interfaces

Functional interfaces are interfaces that contain only one abstract method. They provide the blueprint for lambda expressions. Java provides several built-in functional interfaces in the java.util.function package, such as Predicate, Function, Consumer, and Supplier. And there are many more.

Function and BiFunction functional interfaces

Function and BiFunction are functional interfaces provided by the Java API that represent functions that accept one and two arguments respectively and produce a result . They are part of the java.util.function package introduced in Java 8.

  • Function: The Function interface represents a function that accepts one argument and produces a result. It has a single abstract method called apply(). This interface is commonly used for mapping elements of a stream or transforming data.
@FunctionalInterface
interface Function<T, R> {
    R apply(T t);
}

In Function<T, R> the T represents the input variable data type and R represent the return type of the function. So, in the below example, the square method input is Integer and out is also Integer so we have something like this - Function<Integer, Integer>. If it would have returned something other than Integer, let's say String, we would have written it - Function<Integer, String>. Also, while passing the value we need to use square.apply(5), here this 5 is the input to the apply method which is represented by T.

Functional interfaces will only work with Objects. so you can not pass primitive values (Obviously, you can use the wrapper classes). There is a enough reason for that which I am not going to discuss in this blog post.

Example:

Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // Output: 25
  • BiFunction: The BiFunction interface represents a function that accepts two arguments and produces a result. It has a single abstract method called apply(). This interface is often used for operations that involve two input values.
@FunctionalInterface
interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

Example:

BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
System.out.println(add.apply(3, 5)); // Output: 8

In BiFunction<T, U, R> same as above, T and U represents the two inputs data type and R represents the return type of the function.

Now, let's try to compare the imperative way and delcarative way of writing code with a simple example.

package functionalprogramming.functionalinterface;
 
import java.util.function.BiFunction;
import java.util.function.Function;
 
public class _Function {
    public static void main(String[] args) {
        //imperative way
        int increment = increment(1);
        System.out.println(increment); //Output: 2
 
        //Declarative way - Function takes one argument and produces 1 result;
        int  increment2 = incrementByOneFunction.apply(1);
        System.out.println(increment2); //Output: 2
        int multiply = multiplyBy10Function.apply(increment2);
        System.out.println(multiply); //Output: 20
 
        //Declarative way - Combine the two functions using andThen
        Function<Integer, Integer> addBy1AndMultiplyBy10 = incrementByOneFunction.andThen(multiplyBy10Function);
        int result1 = addBy1AndMultiplyBy10.apply(4);
        System.out.println(result1); //Output: 50
 
        //Imperative way
        System.out.println(incrementBy1AndMultiply(4, 100)); //Output: 500
        //Declarative way -  BiFunction takes 2 argument and produces 1 result
        System.out.println(incrementBy1AndMultiplyBiFunction.apply(4, 100)); //Output: 500
    }
    static int increment(int  number){
        return number+ 1;
    }
    static int incrementBy1AndMultiply(int number, int numToMultiplyBy){
        return (number + 1) * numToMultiplyBy;
    }
    static Function<Integer, Integer> incrementByOneFunction =
            number -> number + 1;
    static Function<Integer, Integer> multiplyBy10Function = number -> number * 10;
 
    static BiFunction<Integer, Integer, Integer> incrementBy1AndMultiplyBiFunction =
            (number, numToMultiplyBy) -> (number + 1) * numToMultiplyBy;
}

Explanation:

This code demonstrates the usage of functional interfaces Function and BiFunction in Java.

  1. Imperative Way:
    • Method increment(int number) increments a number by 1.
    • Method incrementBy1AndMultiply(int number, int numToMultiplyBy) increments a number by 1 and then multiplies it by another number.
  2. Declarative Way:
    • Function<Integer, Integer> is used to increment by 1 (incrementByOneFunction) and multiply by 10 (multiplyBy10Function).
    • BiFunction<Integer, Integer, Integer> is used to increment by 1 and multiply by a specified number.

As you can see, at first glance, we don't see much difference between the imperative way and the functional way of writing code. But trust me, by the end of this blog post, it will make sense to use the functional way of writing code as much as possible. For now, just take a look at this line - Function<Integer, Integer> addBy1AndMultiplyBy10 = incrementByOneFunction.andThen(multiplyBy10Function);. This way, we can chain multiple functions together. andThen returns a composed function that first applies this function to its input, and then applies the after function to the result. If the evaluation of either function throws an exception, it is relayed to the caller of the composed function.

Remaining part coming soon