It has been almost 7 years since Oracle released Java 8, with it, they also released lambda expressions and Functional interfaces, which lets you do functional programming in Java, but it's not very intuitive when compared to languages i.e javascript (functions are not the first-class citizen in java)
In this post, I will give a brief introduction of functional interfaces and lambda expression in Java and try to explain the little strange syntax with one important example.
Functional Interfaces
Some of the important functional interfaces Java 8 has to offer, which we will discuss in a while.
- Function
- Consumer
- Supplier
- BiFunction
- Predicate
Function
Represents a function which accepts one argument and produces a result.
It is a very simple and understandable definition, right? But when you go and try to use it you would get confused at first, because as per definition, this represents something like
R someFunction(T param) {
return r;
}
// Using it would be
R r = someFunction(t);
But in your code, you would write
Function<Integer> convertIntToStr = t -> "Some String from " + t;
String str = convertIntToStr.apply(1);
Got confused, right?
This is because interfaces and classes are the first-class citizens in Java, that's why they have provided these Functional Interfaces and their usage is similar to creating an object and calling functions on that object.
So, what are functional interfaces?
Functional interface is the interface in Java 8 which has only one abstract method, any interface which qualifies this condition is a functional interface and you can also denote it with
@FunctionalInterface
annotation to explicitly state that, so that compiler can generate an error if some try to add another abstract method to it.
If you checkout the implementation of Function, you would see something like this
@FunctionalInterface
interface Function<T,R> {
R apply(T t); //only abstract method
// default method omitted for brevity
}
So, our code example above initialises the object of this interface with lambda expression which is in this case just a function
Function<Integer, String> convertIntToStr = t -> "Some String from " + t;
t -> "Some String from " + t
equivalent to
String someFunction(Integer value) {
return t -> "Some String from " + t;
}
And since convertIntToStr
is an object of type Function, we call the apply method on this object to execute our lambda expression
or someFunction
and you must have got an idea by now that you provided the implementation for apply
function using a lambda expression, because the same statement can be written as anonymous object declaration
Function<Integer, String> convertIntToStr = new Function<Integer, String>() {
@Override
String apply(Integer t) {
return "Some String from " + t;
}
}
So, next time think the statement on left to be equivalent to the right
R someFunction(T param) {
return r;
}
R r = someFunction(t);
Function<T, R> someFunction = t -> r;
someFunction.apply(t);
All other, interfaces work in a similar manner, those represent some other useful functions, Let's see what they have to offer.
Consumer
Represents an operation that accepts a single input argument and returns no result.
You can think of Consumer as void function, which takes a parameter and it has an abstract method void accept(T t)
Supplier
Represent a supplier of results.
You can think of Supplier as a function which doesn't take any argument and return a value and it has an abstract method T get()
BiFunction
Represents a function that accepts two arguments and produces a result.
You can think of BiFunction as a function which takes two arguments and return a value and has an abstract method R apply(T t, U u)
Predicate
Represents a predicate (boolean-valued function) of one argument.
You can think of Predicate as a function which takes an argument and returns a boolean value, it has an abstract method boolean test(T t)
There are more interfaces than mentioned, you can check docs
Why Functional Programming?
Functional programming lets you compose your logic through pure functions in a declarative manner avoiding shared state, mutable data and side effects. Let's see this with a very simple example.
Suppose we have an item and we need to calculate different taxes on the item's base price, we would write something like this
I would be using lombok project to generate some boilerplate code in my examples, so please check its documentation for more info.
@Value
class Item {
String name;
Double price;
}
class TaxCalculator {
// I have passed the boolean flags for the taxes needs to be calculated
public Double calculateTaxablePrice(Item item, boolean excise, boolean vat, boolean cess, boolean gst) {
Double basePrice = item.getPrice();
if(excise) basePrice += basePrice * 0.02;
if(vat) basePrice += basePrice * 0.04;
if(cess) basePrice += basePrice * 0.01;
if(gst) basePrice += basePrice * 0.1;
return basePrice;
}
}
// And we would call like this
Double newPrice = calculator.calculateTaxablePrice(item, true, false, true, true);
You might have spotted the problems in the above code, let me point out a few below
- If we need to add another tax tomorrow, we would change this method and so have to be the every calling method in client code.
- Client will have to dig into the implementation of a function to know, which flag is for which tax, so the client can make a mistake and this may not be captured by the unit test also.
So, what can we do? An alternative approach can be to create a TaxCalculatorBuilder
which will create the TaxCalculator
object for us, which will have all the flags as state variables and calculate
function will take only price
, providing fluent API to the client
TaxCalculator calculator = new TaxCalculatorBuilder()
.withExcise()
.withVat()
.withCess()
.withGst()
.build();
// And we would call like this, here calculator object knows which tax to be applied
Double newPrice = calculator.calculateTaxablePrice(item);
But, there is an issue with the builder as well, you would have to change the builder class every time a new tax is added. So, how can we solve this problem?
We can solve this issue using functional interface Function<T, R>
and we will see in a moment how?, but before that let's define each tax as a function which would take one argument and give back the result, in new class TaxRules
@NoArgsConstructor(access = AccessLevel.PRIVATE)
class TaxRules {
public static Double excise(Double price) {
return price + price * 0.02;
}
public static Double vat(Double price) {
return price + price * 0.04;
}
public static Double cess(Double price) {
return price + price * 0.01;
}
public static Double gst(Double price) {
return price + price * 0.1;
}
}
Now, we would write our improved tax calculator as following
class ImprovedTaxCalculator {
private final List<Function<Double, Double>> taxRules = new ArrayList<>();
public ImprovedTaxCalculator with(Function<Double, Double> fn) {
taxRules.add(fn);
return this;
}
public Double calculateTaxablePrice(Double price) {
Function<Double> fn = taxRules.stream()
.reduce(f -> f, (firstFn, secondFn) -> firstFn.andThen(secondFn))
return fn.apply(price);
}
}
The calculate function here is little difficult to understand so let's apply divide and conquer to understand it
-
taxRules
is a list,.stream()
method call gives us a stream of the object in the list. -
reduce()
method takes a stream of elements and produce a single result.
Let's take an example to understand it
List<Integer> numbers = Arrays.asList(1,2,3,4,5);
Integer sum = numbers.stream()
.reduce(0, (sum, number) -> sum + number);
The Integer value 0 is the identity value, Its the initial value provided to the reduction process and if the value of the list is empty then it will be the default value which will be returned.
The second argument is the accumulator which will add the previous sum to next number from the stream, initially sum would be 0 here
(sum, number) -> sum + number // Lambda expression
Lambda expression can also be written as a method reference
Integer sum = numbers.stream()
.reduce(0, Integer::sum);
Now let's get back to our original example now
Function<Double> compositeFn = taxRules.stream()
.reduce(f -> f, (firstFn, secondFn) -> firstFn.andThen(secondFn));
Here, first function in taxRules would be our identity and accumulator is a lambda expression returning a new composed Function, that first applies its input to firstFn and then applies the result to secondFn.
Now, Let's see how client would calculate tax using our improved calculator
ImprovedTaxCalculator calculator = new ImprovedTaxCalculator()
.with(price -> TaxRules.excise(price))
.with(price -> TaxRules.cess(price))
.with(price -> TaxRules.gst(price));
Double newPrice = calculator.calculateTaxablePrice(item);
If tomorrow, we got a new tax, then we just have to add the new function into TaxRules
class and the client can use it, we don't have to change our calculator.
Also, if you have a case where only one client has to apply some extra tax which doesn't have to be defined in TaxRules
, then the client can very easily do that
ImprovedTaxCalculator calculator =
new ImprovedTaxCalculator()
.with(price -> TaxRules.excise(price))
.with(price -> TaxRules.cess(price))
.with(price -> TaxRules.gst(price))
.with(price -> price + price * 0.03); //Extra
Double newPrice = calculator.calculateTaxablePrice(item);
You can see that how functional interface Function can help us write better reusable code with clean fluent API, which is much better than the builder, we wrote earlier.
For now, I will leave you all with this example, we will see some more examples and functional programming pros and cons in the coming posts. So till then enjoy.....
Top comments (0)