DEV Community

loading...
Cover image for Functional Programming in Java 8, with Example(s)

Functional Programming in Java 8, with Example(s)

Kuldeep Yadav
Developer, love to build things, Husband.
・6 min read

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);
Enter fullscreen mode Exit fullscreen mode

But in your code, you would write

Function<Integer> convertIntToStr = t -> "Some String from " + t;

String str = convertIntToStr.apply(1);
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
}
Enter fullscreen mode Exit fullscreen mode

So, next time think the statement on left to be equivalent to the right

R someFunction(T param) {
    return r;
}
R r = someFunction(t);
Enter fullscreen mode Exit fullscreen mode
Function<T, R> someFunction = t -> r;
someFunction.apply(t);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);

    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Lambda expression can also be written as a method reference

Integer sum = numbers.stream()
                                            .reduce(0, Integer::sum);
Enter fullscreen mode Exit fullscreen mode

Now let's get back to our original example now

Function<Double> compositeFn = taxRules.stream()
    .reduce(f -> f, (firstFn, secondFn) -> firstFn.andThen(secondFn));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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.....

Discussion (0)

Forem Open with the Forem app