Using Java Functional Interfaces

·

4 min read

In my professional world I've been working a lot with Java. There's a lot of things that I like, and don't like, about Java; like most programming languages I've ever done anything serious with. One of the things I like about the language is the way they deal with anonymous functions via the @FunctionalInterface annotation. However, I've come around to the idea that you probably should not be declaring functional interfaces directly in your application code.*

Functional Interface Quick Look

If you're already familiar with the way Java's FunctionalInterface works feel free to skip to the next section!

First, let's take a look at how the Java functional interfaces are used. What better example than the Function interface! It resembles the following:

@FunctionalInterface
public interface Function<ArgumentType, ReturnType> {

    ReturnType apply(ArgumentType argument);

}

Like all functional interfaces, it only has one method and is annotated with @FunctionalInterface. You can easily create concrete implementations of this interface using lambda expressions. For example, assuming we have a type declaration of Function<Foo, Bar> we could satisfy it with the following expressions.

// If there is only one argument and you can keep everything to one logical line 
// there's no need for parenthess, braces, or return statements. Pretty nifty!
Function<Foo, Bar> = foo -> new Bar();

// This is equivalent to the above line. The parentheses are necessary if there's 
// more than one argument
Function<Foo, Bar> = (foo) -> new Bar();

// If you need to do some initialization or something that requires multiple lines
// you'll need to include braces
Function<Foo, Bar> = foo -> {
      Bar bar = BarFactory.createBar();
    bar.setBuzz("qux");
    return bar;
};

You can, of course, introduce a concrete type that implements this interface and satisfy this type declaration with a "normal" object instance. Lambda expressions are pretty powerful in Java and there's additional ways you can satisfy these functional interface type declarations. For now, let's look back at the statement from above.

You probably should not be declaring functional interfaces directly in your application code.

Lack of Expressiveness

When we're dealing with those abstract types with no real example it can be difficult to see the problems with these type declarations. Let's take a look at a real world example that I ran into recently. The Function type that was declared looked like the following:

public class UserRelatedService {

    public UserRelatedService(Function<User, Boolean> userFunction) {}

}

Looking at this would you be able to describe what this userFunction was responsible for? I certainly couldn't. It was pretty quick to figure out once I dug into the code some more. However, the entire point of expressive type systems is to be more communicative and present more useful information to readers of your code! Those readers are your fellow teammates, new team members getting up to speed, and yourself 6 months later when you come back to this code and don't remember what you did.

Making it Expressive

Fortunately, Java makes this fairly straightforward to solve. It could even be simple, as long as you don't run into problems naming what the function is actually being used for. In my example it was a way to handle authenticating a user based on conditions that could vary. So, I introduced a new interface that extends the Function.

@FunctionalInterface
public interface UserAuthenticator extends Function<User, Boolean> {}

public class UserRelatedService {

    public UserRelatedService(UserAuthenticator userAuthenticator) {}

}

I was able to introduce the new type in effectively 2 lines of code but it is already a significant improvement! Now consumers of the UserRelatedService know there's going to be some authentication required and they can start looking for documentation (you do have documentation, right?) about how they might need to authenticate the given user or what standard authentication methods might exist. They know and can intuit all these things simply by looking at the method signature and never having to dig into the code itself. I consider this a huge win!

*The Asterisk in the Intro

At this point in my career I don't like making hard, rigid rules that absolutely apply to every situation. Software needs to be flexible and adapt to changing business needs, tech stack improvements, security flaws, and performance concerns. Throw in the need to keep your codebase readable by other human beings and that can present enough variables, pun intended, to make the only appropriate steadfast rule is that you shouldn't create steadfast rules. However, one of the things I strongly believe helps create systems that are flexible and adaptable is proper use of your type system and being communicative and expressive with your types. For this specific rule I feel pretty comfortable saying you should be doing this the vast majority of the time. When you don't do it, it should be obvious to you and the rest of your team why you didn't.