Home > Back-end >  Why and When should I use Functional Interfaces
Why and When should I use Functional Interfaces

Time:03-22

Say I am writing a Consumer which prints something on the console. Then why shouldn't I directly use the System.out.println() method instead of creating a Consumer?

Similarly, say I want to return a random number, then we can use Random object directly in a normal method to return a number rather than using a Supplier to do so.

So why should one use Consumer and Supplier? What benefits does it bring?

CodePudding user response:

Functional interfaces should of course only be used where it is reasonable, and not everywhere. It is closely related to abstraction and generalization. Good examples for this can be seen in the Java SE API, for example for the java.util.stream.Stream interface. The Stream.map(Function) method cannot know how a user would want to transform elements. Therefore it takes a Function and lets the user implement it in whatever way they like.

Unless you are writing a (general-purpose) library, there is normally no need to directly use functional interfaces yourself for method parameters. Normal inheritance and usage of interfaces should suffice for most use cases.
If you find a situation in your code where a functional interface would help, and the functional interface is needed for a specific usecase, it might be benefitial to define your own one instead of using standard one from the java.util.function package. This allows you to restrict input and output types and write custom documentation, compared to using something as nonexpressive as Function.apply.

CodePudding user response:

a Consumer which prints something on console. Then why shouldn't I directly use the System.out.println() method instead of creating a Consumer?

There's nothing wrong with printing something by simply using System.out.println(). When it's a single almost isolated action like in your example, there's no room and no need to entail anything else. Conversely, if you would write in the main something like that it would look contrived:

Consumer<String> myConsumer = System.out::println;
myConsumer.accept("Hello Functions!");

There's no reason to replace a single statement that is clear to every beginner by the consumer here. Don't add the functionality that is not required, as the YAGNI principle suggests.

Let's look at the cases when the functional interfaces can be beneficial.

An Interface means a Contract

First of all, interfaces are meant to provide contract.

Thy are particularly useful when you want to generalize something, and functional interfaces are not an exception.

Let's consider the following example.

You have a couple of methods. One logs a message, another send some kind of request, the third sends an email, etc. And all these methods expects the same object, let's say of type Event as parameter. That means, each of these methods could be used as an implementation of Supplier<Event>.

Also, we have a group of methods that are calling these methods. And maybe they are not differ a lot. Let's assume each of the methods in this group is responsible for performing some kind of specific checks, then it processes the data and invokes one of the methods mentioned above.

With that, we can potentially refactor this second group of methods by contracting it to a single generalized method with such a signature:

public void processEvent(List<Event> events, 
                             UnaryOperator<Event> operator, 
                             Predicate<Event> predicate, 
                             Consumer<Event> consumer)

Leave the Streams alone

There is a delusion that functions could be used only with streams. No, that totally fine if, let's say, your method that doesn't utilize stream anyhow expects a predicate (or other functional interface build in the JDK). That allow you to make the behavior of your classes more flexible. You can define a predicate inline when calling a method

doSomething(person, person -> person.getAge() > 30);
doSomething(person, person -> person.getAge() <= 25);
doSomethingElse(token, MyToken::isValid);

And inside the method, use it like that:

if (myPred.test(token)) { // do }

Another option is you to maintain a collection of functions in order to reuse the behavior they are representing and provide convenient access to them. For instance:

Map<MyEnum, Function<EventDTO, Event>> funcByEnum = ...;
Event myEvent = funcByEnum.get(enum).apply(dto);

Implementing Functional interfaces

Also, your classes can implement functional interfaces.

As an example from the JDK - a group of summary statistics classes like IntSummaryStatistics that are implementing different flavors of consumer. And you can modify a summary statistics object by using method accept():

stat.accept(1000);

That allows to benefit from the self-explanatory names of methods defined by build in functional interfaces.

Names test, accept will be easily understood by anyone needs to read code (assuming they meat with a purpose of your methods).

  • Related