I have recently been studying functional programming concepts with Haskell to better understand how to write code in a declaritive manner. This study has led to me digging into how I can apply some of those concepts in Java. You don't have to look too deep into "Functional Java" to discover Java's functional interfaces. Below is a summary of some of the basic concepts around the Java functional interface. Of course, there are other functional concepts in Java (like streams); but I will try and explore those in other posts.
What is a Functional Interface?
An interface needs one (and only one) abstract method to be considered a functional interface.
// This is a Functional Interface
interface MyFunctionalCalculator {
int multiplier(int a);
}
Java gives us an informational annotation @FunctionalInterface.
@FunctionalInterface
interface MyFunctionalCalculator {
int multiplier(int a);
}
According to the documentation, the annotation gives us the following advantage:
If a type has this annotation type, compilers are required to generate an error message unless:
- The type is an interface type and not an annotation type, enum, or class.
- The annotated type satisfies the requirements of a functional interface (one and only one abstract method).
I recommend using the annotation because it helps document the intent of the interface.
Great, how can I use them?
In my mind, the real advantage of a functional interface is that it provides a target type for lambda expressions and method references. This feature allows us to pass lambda expressions as parameters and utilize function composition to write declarative code.
We can write code like the following:
MyFunctionalCalculator calc = a -> a * 2;
calc.multiplier(2); // returns 4
or something like this:
int multiplierPlusOne(MyFunctionalCalculator calc, int a) {
return calc.multiplier(a) + 1
}
MyFunctionalCalculator doubler = a -> a * 2;
MyFunctionalCalculator trippler = a -> a * 3;
multiplierPlusOne(doubler, 2); // returns 5
multiplierPlusOne(trippler, 2); // returns 7
If you have worked with Java Streams before, you are familiar with this concept. Many of the Stream methods take a functional interface as a parameter. You may have used the Stream Filter method before.
// Predicate is a Functional Interface
// definition of filter
filter(Predicate<? super T> predicate)
// looks something like this when used.
personList.stream().filter(p -> p.age > 21) // the (p -> p.age > 21) is the Predicate
According to the docs, the filter method returns a stream consisting of the elements of the stream that match the given predicate. Predicate is a Functional Interface that is provided by the Java Util Package.
Some of Java's built-in Functional interfaces
The Java Util Package gives us quite a few handy functional interfaces that we can use for many different cases. Below is a summary of some of the functions from the docs.
Function
Function represents a function that takes in an argument then returns a result. It looks like this:
@FunctionalInterface
public interface Function<T,R>
Consumer
Consumer takes in a single argument and returns no results. The docs say that it is expected to operate via side effects.
@FunctionalInterface
public interface Consumer<T>
Supplier
Supplier supplies a result.
@FunctionalInterface
public interface Supplier<T>
Predicate
Represents a predicate (boolean-valued function) of one argument.
@FunctionalInterface
public interface Predicate<T>
BiFunction
BiFunction takes two arguments and returns a result.
@FunctionalInterface
public interface BiFunction<T,U,R>
Many of the functions have a "Bi" version which allows for two arguments. There are many more functional interfaces available for use in the java util package. Make sure you check out the docs to see what else is available. Now, let's see if we can code something more useful using some of these functional interfaces.
A more "real world-ish" example
Here is a small example of how we can use Functional Interfaces to help us build re-usable code. Recently I came across some code where I thought I could use a Functional Interface combined with streams to write more declarative code. Below is an example of how I was able to do this.
The code has an Enum called Galaxy that represents the various galaxies referenced in our system. This Enum is a key to load values into an MDC (Mapped Diagnostic Context) logger. MDC acts a bit like a Map that stores Galaxy values for logging purposes.
public enum Galaxy {
ANDROMEDAE("andromedae"),
ANTENNAE("Antennae"),
EYE_OF_SAURON("eyeOfSauron"),
MEDUSA_MERGER("medusaMerger");("andromedae"),
ANTENNAE("Antennae"),
EYE_OF_SAURON("eyeOfSauron"),
MEDUSA_MERGER("medusaMerger");
private String key;
// puts value in MDC
public void put(final String value) {
MDC.put(key, value)
}
// constructor and getter removed for brevity...
}
While working with some code elsewhere in the application, I ran into something like this. The code below finds a star, then checks the MDC to see if it contains any Galaxies. For each Galaxy found in the MDC, it does some logging.
Star foundStar = starRepository.getStarById(1L);
for (final Galaxy g :: Galaxy.values()) {
final String gal = MDC.get(g.getKey())
// if the galaxy is loaded in the MDC
if(gal != null) {
// do work with found galaxy
logStarToGalaxy(foundStar, gal);
}
}
Now, this code is ok in my opinion, but I want to make this code more declarative and less imperative. I know I want to use a Stream instead of a loop. To do this, I will need to add a helper method to the Enum.
public enum Galaxy {
ANDROMEDAE("andromedae"),
ANTENNAE("Antennae"),
EYE_OF_SAURON("eyeOfSauron"),
MEDUSA_MERGER("medusaMerger");
private String key;
// puts value in MDC
public void put(final String value) {
MDC.put(key, value)
}
// constructor and getter removed for brevity...
public static stream<Galaxy> stream() {
return Stream.of(Galaxy.values());
}
}
I just added a simple method that returns the Enum values as a Stream. Now, I can refactor the for loop in the code.
Galaxy.stream().forEach(g -> {
final String gal = MDC.get(g.getKey())
// if the galaxy is loaded in the MDC
if(gal != null) {
// do work with found galaxy
logStarToGalaxy(foundStar, gal);
}
OK not bad, but this did not really make the code more readable. Let's see if we can use a filter and a Functional Interface to get rid of the if-statement. We know from the above documentation that the filter method takes a Predicate as a parameter. The filter logic is straight forward--we want to only work with Galaxies that are found in the MDC. Since this logic seems like it might be used in several places, I add the Predicate to the Enum as a static Function.
public enum Galaxy {
ANDROMEDAE("andromedae"),
ANTENNAE("Antennae"),
EYE_OF_SAURON("eyeOfSauron"),
MEDUSA_MERGER("medusaMerger");
private String key;
// constructor, other methods, and getter removed for brevity...
public static final Predicate<Galaxy> isInMDC = key -> MDC.get(key.getKey()) != null;
}
Now that I have the Predicate, I can use that in a filter.
Galaxy.stream()
.filter(Galaxy.isInMDC)
.forEach(g -> logStarToGalaxy(foundStar, g.getKey()));
So we went from this:
for (final Galaxy g :: Galaxy.values()) {
final String gal = MDC.get(g.getKey())
if(gal != null) {
logStarToGalaxy(foundStar, gal);
}
}
to this:
Galaxy.stream()
.filter(Galaxy.isInMDC)
.forEach(g -> logStarToGalaxy(foundStar, g.getKey()))
This code is clean and easy to understand. This code is "declarative" (telling the computer what we want it to do), and we have removed the if statement by using the stream filter and a Predicate. We also have the advantage of being able to reuse that Predicate function wherever else we need to check the MDC.
The above code is just one example of using functional interfaces. Functional interfaces also allow us to compose functions together, helping us make our code in small, re-usable blocks that end up being declarative in style. The above information is only a summary of the Java functional interface, and there is a lot more you can dive into with functional interfaces. You can see they are closely related to using Lambdas and are used heavily in Streams. I encourage you to look into those concepts while you are exploring functional interfaces. If you have any other helpful tips about functional programming in Java or suggestions on how to make the code above better, throw up a comment and join the conversation!
Top comments (0)