Be More Functional with Java’s Functional Interfaces

Hey! tea lovers. This post is about the functional interfaces provided by Java. We will talk about the basic ones briefly. These functional interfaces are used by Stream API heavily, so knowing them will give you a huge advantage. Not just streams you can use it anywhere.

Necessities before diving in

Not much, Just make sure the concept of functional interfaces and lambdas are clear. You can find the code on GitHub here or the full project here. And yes fill your cup of tea to sip and learn with me.

java.util.function

The functional interfaces we will be discussing in your tea break will be java.util.function. This package contains Java’s general-purpose functional interface to be used by coders like us. This helps us to save time by not writing it repeatedly. It has many functional interfaces (you probably won’t be needing that many in your code) but we are going to see the most common and useful functional interfaces. We will be comparing it to the normal method or function for better comprehension

Consumer <T>

As the name implies Consumer consumes the inputs. It doesn’t return anything when called. It is similar to function with the void return type. This functional interface has 2 methods one being the void accept(T t) to trigger it and the other is the default method andThen(Consumer<T>) which lets you chain multiple Consumers. You can call multiple Consumers with andThen(consumer1).andThen(consumer2)..... and call accept(t) function to terminate the chain. All Consumers in this chaining will use the same input given via accept().

Consumer<Integer> printAgeConsumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer age) {
                System.out.println("Age is " + age);
            }
        };

//call the method
printAgeConsumer.accept(23);// Age is 23

//using lambda
Consumer<Integer> printAgeWithLamda = (age) -> System.out.println("Lamdda : age is " + age);
//will work similar as printAgeConsumer
printAgeWithLamda.accept(223);//Lamda : age is 223

//chaining with andThen(Consumer)
printAgeConsumer //1st
        .andThen(printAgeWithLamda)//2nd
        .andThen(age -> System.out.println("How old is he ? " + age))//3rd
        .accept(23);//this value will be given to each consumer

Predicate <T>

The Predicate is used for boolean values. It tests the given value and returns a boolean by calling test(T t). Then again like Consumer, you can use chaining using and(anotherPredicate). And calling test(T) in the end, it will complete the chain. Similar to Consumer they use the same input throughout the chain. It returns true only if all the predicates return true else it returns false.

 Predicate<Integer> isEven = new Predicate<Integer>() {
            @Override
            public boolean test(Integer number) {
                return number % 2 == 0;
            }
        };
boolean is23Even = isEven.test(23);//false

//using lambda
Predicate<Integer> isEvenlambda = (number) -> number % 2 == 0;
boolean is46Even = isEvenlambda.test(46);//true

//chaining with and()
//returns true only of all the predicated returns true else false
isEven // 1st - number should be even
        .and(n -> n < 10)//2nd -number should be less than 10
        .and(n -> n > 5) // 3rd should be greater than 5
        .test(6); // this nu,ber will be used in chain
// returns true after completion

Function <T, R>

Yes, that’s the name. Unlike preceding interfaces, Function has two generic parameters. The first parameter is for input type and the second one is for the return type. Essentially Function is used for transforming the given value into the desired one. Functions use apply(T t) for its functions and andThen(anotherFuntion) for chaining. But dissimilar to Consumer and Predicate this chaining does not use the same value to all the Functions instead, they use the result of the preceding Function. But keep in mind that, the subsequent Function has the input type equivalent to the current Function’s return type.

Function<Integer, Integer> doubleTheNumber = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer number) {
                return number * 2;
            }
        };
int doubleTheNumber2 = doubleTheNumber.apply(2);//4
//using lambda
Function<Integer, Integer> subtract2 = (number) -> number - 2;
int subtract2From4 = doubleTheNumber.apply(4);
System.out.println(subtract2From4);

//chaining with andThen()
//Unlike Predicate and Consumer which uses the same value to all the nodes,
//Function uses the result of previous Function
doubleTheNumber // 1st will double the number
        .andThen(subtract2) // 2nd will subtract 2 from doubled number
        .andThen(doubleTheNumber) // 3rd subtracted result will be doubled
        .apply(4); // (((4 * 2) - 2) * 2) = 12

BiFunction <T, U, V>

BiFunction is similar to Function but it accepts two input parameters as opposed to Function which takes only one input. It has apply(T t, U u)  function for triggering it. Chaining with andThen in BiFucntion is a little different. It uses Function as a parameter in the andThen as andThen(Function<V v, E e>) ( since methods don’t have two return values).

BiFunction<Integer, Integer, Integer> areaOfRectangle = new BiFunction<Integer, Integer, Integer>() {
    @Override
    public Integer apply(Integer length, Integer breadth) {
        return length * breadth;
    }
};

areaOfRectangle.apply(2, 2);//4

BiFunction<Integer, Integer, String> areaWitMessage = (lengh, breadth) -> "Area is " + lengh * breadth;

areaWitMessage.apply(4, 3);// Area is 12

//chaining
areaOfRectangle // 1st area will be calculated
        .andThen(area -> area * 3) // 2nd calculated area will be multiplied by 3 as height
        .apply(2, 3);// 2 * 3 * 3 = 18

Supplier < T >

Till now we have talked about the interfaces which take input parameters. Supplier, on the other hand, does not demand any arguments, it just produces one by calling get(). Completely different than selfish Consumer, a taker, Supplire is a giver. It like a method which produces values without input, like toString() or hashCode(). We can’t use chaining in this since it does not take any input parameters.

Supplier<Double> randomSupplier = new Supplier<Double>() {
    @Override
    public Double get() {
        return Math.random();
    }
};
double randomNumber = randomSupplier.get();
System.out.println(randomNumber);

//lambda
Supplier<String> codersTeaUrl = () -> "https://coderstea.com";
System.out.println(codersTeaUrl.get());

There are more of them

The ones we talked about are popular and are used in Stream API heavily. But there are more functional interfaces. They are similar to these functions in one or another way. Don’t worry, we don’t have to understand every functional interface in java. If you understood the above-mentioned interfaces, its more than enough.

Still just to name a few, the following table is obtained from the Java doc. you can find it here

InterfaceDescription
BiConsumer<T,U>Represents an operation that accepts two input arguments and returns no result.
BiFunction<T,U,R>Represents a function that accepts two arguments and produces a result.
BinaryOperator<T>Represents an operation upon two operands of the same type, producing a result of the same type as the operands.
BiPredicate<T,U>Represents a predicate (boolean-valued function) of two arguments.
BooleanSupplierRepresents a supplier of boolean-valued results.
Consumer<T>Represents an operation that accepts a single input argument and returns no result.
DoubleBinaryOperatorRepresents an operation upon two double-valued operands and producing a double-valued result.
More functional interfaces

Conclusion

That’s it for the post. In this post, we learned about the inbuilt functional interfaces in Java. These functional interfaces are the backbone of the Stream API. I have written a post on the same topic titled “Stream API: The Hero Without A Cape“. You can find the code on GitHub here or the full project here. See you in the next post.