Functional Programming in Java
Overview
Functional programming is a subset of the Declarative programming paradigm. Functional programming technique makes our code more concise, readable, and predictable. It is easy to test and maintain code developed through functional programming. Functional programming involves crucial concepts such as immutable states, referential transparency, method references, and high-order, and pure functions. It involves programming techniques such as functional composition, recursion, currying and functional interfaces.
Read Also, Complete Java Tutorial
Introduction to Programming Paradigm
Programming Paradigm is a way or style of programming. The programming paradigm can be classified into two types:
- Imperative programming paradigm
- Declarative programming paradigm
Let us quickly go through each of these programming paradigms.
Imperative Programming Paradigm
Imperative programming paradigm consists of a sequence of statements that changes the program's state until the target result is achieved. The main focus is on how to achieve the goal. It consists of three main programming approaches:
- Procedural programming
- Object Oriented programming
- Parallel Processing Approach
Example:
Output
Explanation:
This example defines a sequence of steps that tells the compiler to find the sum from 1 to 5.
Declarative Programming Paradigm
Declarative programming is a paradigm in which we define what needs to be accomplished without defining how it has to be implemented. In the declarative programming paradigm, for the same input arguments, the program produces the same result. The order of execution of statements is not important in the declarative programming paradigm. It consists of three main programming approaches:
- Logic programming paradigm
- Functional programming
- Database processing approach
Example:
Output:
Explanation:
In this example, the input array is converted to stream. The filter() method is invoked for each value in the stream and it is evaluated with the expression a -> a % 2 == 0.
The filter() method passes the value to the next operation i.e. toArray() only if the expression inside it evaluates to true. So, in this case, only 2 and 4 are passed to toArray() operation, and hence the output is [2, 4].
Here, we didn't define the steps to filter the even numbers. Instead, we told the compiler what we want via the expression filter(a -> a % 2 == 0).
What is Functional Programming in Java?
Functional programming is a paradigm where the basic unit of computation is a function. Here functions are not the methods we write in programming. Methods are procedures where we write a sequence of instructions to tell the computer what to do. In functional programming, functions are treated like mathematical functions where we map inputs to outputs to produce a result. Eg. .
In functional programming, the software is developed around the evaluation of functions. The functions are isolated and independent, and they depend only on the arguments passed to them and do not alter the program's state like modifying a global variable.
Functional programming is based on diverse concepts such as First-Class Citizens, Pure Functions, Immutability and Referential Transparency and techniques like Function Composition, Recursion, Monads, Currying, etc. We will learn about these concepts in the upcoming sections.
Functional Programming Vs Purely Functional Programming
Functional programming and purely functional programming prioritize functions but differ in their treatment of state and mutability. Functional programming allows state and mutable variables, while purely functional programming emphasizes immutability and avoids side effects.
Functional Programming | Purely Functional Programming | |
---|---|---|
Paradigm | Programming paradigm that treats computation as the evaluation of mathematical functions. | Subset of functional programming where programs are purely composed of functions without side effects. |
State and Mutation | Allows state and mutable variables. Functions may have side effects and modify variables outside their scope. | Does not allow state or mutable variables. Functions are referentially transparent and do not produce side effects. |
Immutability | Encourages immutable data structures and values. Functions should not modify data but produce new values. | Requires strict immutability. All data is immutable, and any transformations create new data structures. |
Data Encapsulation | Encourages encapsulation of data and behavior using data structures and objects. | Relies on immutable data structures and functional composition to achieve encapsulation. |
Ease of Reasoning | Functions are easier to reason about compared to imperative programming. | Provides stronger guarantees for reasoning due to referential transparency and absence of side effects. |
Concurrency | Allows shared mutable state and requires synchronization mechanisms for thread safety. | Promotes immutable data and pure functions, making it easier to reason about and parallelize code without locks or synchronization. |
Examples | Java with functional programming features (e.g., lambda expressions, streams) | Haskell, Erlang, Lisp, PureScript |
How to Implement Functional Programming in Java
Anonymous Class
An anonymous class is an inner class without a name, which means that we can declare and instantiate a class at the same time. An anonymous class is used primarily when we want to use the class declaration once. Anonymous classes usually extend a subclass or implement an interface.
Example:
Output
Explanation:
In this example, we created an anonymous inner class i.e. new Comparator<Integer>() {} that implements the Comparator interface and overrides the compare() method.
Lambda Expression
Lambda expression is an anonymous function that takes in parameters and returns a value. It is called an anonymous function because it doesn't require a name.
Syntax:
Example:
Output
Explanation:
In this example, we used the reduce() method to find the sum of elements in the list. reduce() accepts two parameters, an initial value, and a lambda expression. Here we passed the lambda expression (a, b) -> a + b.
Method Reference
In certain cases, lambda expression calls an existing or inbuilt method in Java. In such instances, it is clear to call the method by name instead of using a lambda expression to invoke the method. This makes the code more compact and readable.
Syntax:
Example:
Output
Explanation
In this example, instead of using the lambda value -> System.out.print(value) as parameter to the forEach() method, we used System.out::print . This is where we have used Method Reference. During each iteration, each value of the list will be passed to the print() method of System.out object.
Characteristics of Functional Programming Language
First Class Citizens
In Functional programming, functions are considered first-class citizens. A function is called a first-class citizen if it can be:
- stored in a variable
- passed as an argument to a function
- returned as a value from other functions
All these operations are achieved using functional interfaces introduced in Java 8.
Functional Interfaces
A functional interface is an interface that contains exactly one abstract method. It is also called Single Abstract Method (SAM) Interfaces. It can have any number of default methods. In Java, an interface is annotated with @FunctionalInterface to make it a functional interface. @FunctionalInterface annotation ensures that the interface can't have more than one abstract method. Functional interfaces are meant to be used with lambdas.
Example:
Output:
Explanation
- We defined a functional interface Concatenator with only the concat() method. The concat() method accepts two strings as input and returns the concatenation of those strings.
- We stored the implementation of concat as a lambda expression (s1, s2) -> s1 + s2 in the variable concatenator.
- The lambda expression will be treated as the abstract method concat()'s implementation.
- The concatenator is passed as an argument to the method execute() that calls concatenator.concat().
- Here, (s1, s2) -> s1 + s2 is an anonymous function and it is stored in a variable (concatenator) and passed as an argument to the execute() method just like other primitive types.
java.util.function.Function
The java.util.function.Function interface is part of the Java functional programming concept introduced in Java 8. It represents a function that takes an input of type T and produces an output of type R. It is commonly used in scenarios where you need to transform or map values from one type to another.
The Function interface has a single abstract method called apply, which is responsible for performing the transformation. The method signature is as follows:
Here, T represents the type of the input value, and R represents the type of the output value.
Example 1: Transforming a String to its length
Example 2: Converting temperature from Celsius to Fahrenheit
In these examples, the Function interface is used to define a lambda expression that performs the desired transformation. The apply method is then invoked on the function object, passing the input value, and returning the transformed output.
The Function interface provides additional methods for composing functions, such as andThen and composes, allowing you to combine multiple functions together to form more complex transformations.
Functional interfaces like Function enable a more functional programming style in Java, making code more concise, reusable, and expressive.
java.util.function.BiFunction
The java.util.function.BiFunction interface is another functional interface in Java that represents a function that takes two input arguments of types T and U and produces an output of type R. It is commonly used when you need to apply a function to two input values and obtain a result.
The BiFunction interface has a single abstract method called apply, which is responsible for performing the operation. The method signature is as follows:
Here, T represents the type of the first input value, U represents the type of the second input value, and R represents the type of the output value.
Example 1: Concatenating two strings
Example 2: Calculating the sum of two integers
In these examples, the BiFunction interface is used to define lambda expressions that take two input values and perform the desired operation. The apply method is then invoked on the function object, passing the input values, and returning the computed result.
The BiFunction interface provides additional methods for composing functions, such as andThen and compose, allowing you to combine multiple functions together to form more complex operations.
TriFunction
If you need to work with a function that takes three input values and produces an output, you can define your own functional interface using the @FunctionalInterface annotation. Here's an example:
With this custom TriFunction interface, you can define lambda expressions or method references that take three inputs and produce a result.
Example: Calculating the volume of a rectangular prism
In this example, the TriFunction interface is used to define a lambda expression that calculates the volume of a rectangular prism using its length, width, and height. The apply method is then invoked on the TriFunction object, passing the three input values and returning the computed result.
By defining custom functional interfaces like TriFunction, you can extend the capabilities of Java's functional programming paradigm to handle functions with any number of inputs and outputs.
Lazy Evaluation
Functional programming supports lazy evaluation which is also called by the name: call-by-need evaluation.
Lazy evaluation is an evaluation strategy that delays the evaluation of an expression until its value is needed (non-strict evaluation) and which also avoids repeated evaluations.
Lazy evaluation is based on two principles:
- Don't evaluate an expression if the value is not needed.
- Don't re-evaluate an expression if the value is not needed more than once.
Example:
Output
Explanation:
- When we see the above code, we expect the following output:
- However, Java streams use lazy evaluation, and it doesn't evaluate the expression until its result is needed.
- The result of the filter() method is needed only for the stream.collect() method.
- So, the filter() method is evaluated only when the compiler executes the steam.collect() method.
Stateless
Functional programming doesn't contain any state. It doesn't alter the state of a program during execution.
Consider the above methods factorialImperative() and factorialDeclarative() for calculating the factorial of a number.
The factorialImperative() method uses the variable ans to assign the values during each iteration repeatedly. So, it is changing the state of the program.
This factorialDeclarative() method uses recursion to compute the factorial of a number. It doesn't use any variables, only uses method arguments, and doesn't alter the program's state.
No Flow Controls
Functional programming doesn't use flow controls like if..else, switch, for, while, do..while. It is completely based on function calls.
Principles and Concepts of Functional Programming
First-Class Functions
A function is said to be First-Class if treated like other variables in a programming language. A First-Class function satisfies the below criteria.
- A function can be assigned to a variable.
- A function can be passed as an argument to another function.
- A function can be returned from another function.
Higher-Order Functions
A function is said to be Higher-Order if it satisfies any criteria below.
- A function receives another function as an argument.
- A function that returns a new function.
Example
Output
Pure Functions
A function is said to be pure if it satisfies the below criteria.
- A function returns the same output, given the same input.
- A function doesn't have any side effects (i.e.) modifying global or local state or saving data to a database or disk.
The function sum() is pure because it operates only on the arguments a and b. The sum() function returns the same output, given the same input, and doesn't modify any local or global state.
Immutability
Immutability means that an object cannot be modified once created. Java's immutability can be achieved using the final keyword and private access modifier. An immutable class is created in Java using the following rules.
- The class must be declared as final, which prevents it from subclassing.
- All fields must be private and final.
- The class should have one or more public constructors for instantiation.
- The class should have only getters and not setters.
Example
Referential Transparency
An expression is referentially transparent if it can be replaced with its corresponding value without changing the program's behavior.
If all functions involved in the expression are pure and immutable functions, then the expression is referentially transparent. As a result of referential transparency, we produce context-free code. This means the functions can be executed in any order to explore optimization possibilities.
Example:
Output:
Explanation:
In this example replacing sumReferential(2, 3) with the value 5 will give the same result 6. But if we replace sum(2,3) with 5, we will miss the printf statement inside sum() method. So, sum() is not referentially transparent.
Functional Programming Techniques
Function Composition
Function composition is the process of composing bigger functions by combining smaller functions. Function composition can be achieved in Java by Predicate and Function functional interfaces.
Predicate
Predicate is a functional interface that accepts exactly one input and can return a boolean output. It provides built-in methods like and(), or(), negate(), and isEqual() methods to compose multiple functions. The composed function can be executed using the test() method. The syntax is Predicate<T> where T is the input type. Java also provides BiPredicate that accepts two inputs.
and()
Returns true only if all the predicates return true, else false. It behaves like logical AND.
or()
Returns true if any of the predicates return true, else false. It behaves like logical OR.
negate()
Returns true if the predicate returns false and vice versa. It behaves like logical NOT.
isEqual()
Returns a predicate that checks if two values are equal. The equality is decided via Object.equals() method.
Example:
Output:
Explanation:
- isDivisibleByTwo and isDivisibleByFive predicates are composed using and() to form isDivisibleByTwoAndFive predicate. isDivisibleByTwoAndFive returns true only if both the predicates return true, else false.
- isDivisibleByTwo and isDivisibleByFive predicates are composed using or() to form isDivisibleByTwoOrFive predicate. isDivisibleByTwoOrFive returns true if either of the predicate returns true, else false.
- isDivisibleByTwo.negate() returns true if the value is not divisible by 2, else false. This is because the actual result is negated.
- Predicate.isEqual() returns true only if the parameters to isEqual() and test() method are equal. The result is false in the example because Tony and Stark are not equal.
Function
Function is also a functional interface that accepts exactly one input and can return any output. Function provides built-in methods like compose() and andThen() to compose multiple functions. The composed function can be executed using the apply() method. The syntax is Function<T, U> where T and U are the type of input and output, respectively. Java also provides BiFunction that accepts two inputs.
compose()
compose() method first executes the predicateA with value and then executes predicateB with the result of predicateA. The above statement can be expanded as below.
andThen()
andThen() method first executes the predicateB with value and then executes predicateA with the result of predicateB. The above statement can be expanded as below.
Note:
predicateA.compose(predicateB) is equivalent to predicateB.andThen(predicateA) and produces the same result.
Example:
Output:
Explanation:
- appendXAndThenY.apply("A") returns A-X-Y because it is composed with andThen() and appendX is executed before appendY.
- appendYAndThenX.apply("A") returns A-Y-X because it is composed with compose() and appendY is executed before appendX.
Monads
Monad is a technique that allows us to wrap a value and apply a series of transformations to it. Monads should follow the three laws, left identity, right identity, and associativity. Optional is a good example of the monad in Java.
Example:
Output:
Explanation:
In this example, we wrapped the value using the of method and applied a series of transformations using the flatMap method. Each flatMap accepts an input and returns an Optional.
Currying
Currying is converting a function with multiple arguments into multiple functions with a single argument.
Example:
Output:
Explanation:
In this example, we created two methods, concatTraditional() and concatCurried(). concatTraditional() is the standard concat method that accepts two strings, whereas concatCurried() has a sequence of methods that takes a single string as an argument.
Currying is incorporated via the lambda expression a -> b -> a + b. We can split this expression into two different expressions as below.
- a is input and b -> a + b is output.
- b is input, and a + b is output.
Recursion
Recursion is the technique of making a function call itself. Since the function is called repeatedly, a base condition is required to break the loop.
Example:
Output
Explanation:
The factorial() method is initially called with the value 5 from the main() method. During each recursive call, the value of n is decremented by 1. When the value of n becomes 1, the recursive call breaks, and the value is returned to the main() method. The base condition to break the recursive call is if (n == 0 || n == 1) return 1;
Why does Functional Programming Matter?
Our ability to decompose a problem into parts depends directly on our ability to glue solutions together. To support modular programming, a language must provide good glue. Functional programming languages provide two new kinds of glue — higher-order functions and lazy evaluation.
-- Why functional programming matters by John Hughes
- Functional programming operates on functions, and a function is responsible for accomplishing a particular task. This encourages the concept of modularity.
- A program can be divided into several modules where each module is responsible for a specific task. Each module can further be divided into several modules to handle atomic tasks.
- The biggest advantage of adopting functional programming is pure functions and immutable states. The implementation of these two techniques makes the code easily maintainable and debuggable.
- As a subset of declarative programming, functional programming provides several other benefits like lazy evaluation, high-order functions, functional composition, and chaining.
Functional Programming Paradigm vs. Object-Oriented Programming Paradigm
Functional Programming Paradigm | Object-Oriented Programming Paradigm |
---|---|
Functional programming follows declarative programming paradigm. | Object-Oriented programming follows imperative programming paradigm. |
Functional programming uses immutable data. | Object-Oriented programming uses mutable data. |
Functional programming supports parallel programming. | Object-Oriented programming doesn't support parallel programming. |
Functional programming operates on variables and functions. | Object-Oriented programming operates on objects and methods. |
Functional programming uses recursion for iteration. | Object-Oriented programming uses loops for iteration. |
Functional programming is stateless. | Object-Oriented programming is stateful. |
Advantages of the Functional Programming
- Functional programming uses pure functions, which only depend on the input parameters passed to the function and always produced the same output for the given input.
- Functional programming deals with immutable data (i.e.) the state doesn't change, so it is easier to debug.
- Functional programming supports parallel programming because pure functions don't change the state and operate exclusively on the input parameters.
- Functional programming supports lazy evaluation, which avoids unwanted and repeated computations and saves time.
- Functional programming is thread-safe because pure functions don't modify the state of a program, and there is no shared state.
- In functional programming, functions can be stored in variables and passed to other functions. This enhances the readability of code.
Conclusion
- Programming Paradigm is a way or style of programming and can be classified into imperative and declarative.
- Functional programming is a type of declarative programming paradigm.
- Functional programming operates on immutable data and is stateless, meaning it doesn't alter the program's state.
- Functional programming supports lazy evaluation, parallel-programming and it is thread-safe.
- Functional programming is based on First-Class Functions, Pure Functions, Immutability, and Referential Transparency.
- Function composition, currying, monads, and recursion are functional programming techniques.
- Functional programming encourages modularity where complex functions are subdivided into simpler ones.