Understanding Java Functional Interfaces

Here are the types and details of functional interfaces.

Nuwan Zen
CodeX
7 min readSep 4, 2021

--

Photo by Killian Cartignies on Unsplash

A functional interface is the type used for a parameter when a lambda
expression or method reference is passed as an argument to a method.

What are lambdas?

Lambdas are succinctly expressed single method classes that represent behavior introduced in Java 8. They can either be assigned to a variable or passed around to other methods just like we pass data as arguments.

// Concatenating strings
(String s1, String s2) -> s1+s2;

Just like a normal Java method, a lambda expression has optional input arguments, a body, and an optional return value.

input-arguments -> body

Lambda’s Type

You might wonder what the type is for these expressions. As Java is a strongly typed language, it is usually mandatory to declare types; otherwise, the compiler laughs at us. however, that we omitted types when declaring the above lambda expressions. So, what is the type of a lambda expression? Is it a string, an object, or a new functional type? The type of any lambda is a functional interface.

Interface vs Functional Interface

Interfaces are classes that don’t have all the methods implemented, so whoever using the interface must implement the logic. With the introduction of Java 8, this definition has been changed, now interfaces can have implementation logic as default methods, that is we can give a default implementation where others can override them if required. Java 8 also introduced another way of having non-implemented methods called static methods.

Functional Interface is a variation of a Java Interface where only one unimplemented(abstract) method is allowed while any number of default and static methods are allowed. Optionally it can be decorated with an optional @FunctionalInterface annotation. This allows the compiler to generate an error if the annotated interface does not satisfy the conditions.

Sample

Let’s create a functional interface as below:

@FunctionalInterface
interface IAddable<T> {
// To add two objects
public T add(T t1, T t2);
}

Now we can use it

IAddable<String> iAddable = (t1, t2) -> t1+" "+t2+"!";
System.out.println(iAddable.add("Hi","There"));

IAddable<Integer> integerIAddable = (t1, t2) -> t1+t2;
System.out.println(integerIAddable.add(3,5));
//Output
//Hi There !
//8

What is important is now you can remove the business logic from the class implementation and pass as an argument.

Anonymous Classes

If we didn’t use lambda see how the things will be messy. The business logic is shadowed by technical garbage.

IAddable<String> stringIAddable = new IAddable<String>() {
@Override
public String add(String t1, String t2) {
return t1+t2;
}
};

Lambda Expressions vs Inner/Anonymous Classes

When we use an inner class, it creates a new scope. Can hide local variables from the enclosing scope by instantiating new local variables with the same names. Keyword this inside our inner class can use as a reference to its instance.

Lambda expressions, however, work with enclosing scope. Can’t hide variables from the enclosing scope inside the lambda’s body. In this case, the keyword this is a reference to an enclosing instance.

Java Is Intelligent Now

Even though we do not send any type of information with the lambda, java will identify the type based on the assigned variable, below is a string not an integer.

IAddable<String> iAddable = (t1, t2) -> t1+" "+t2+"!";

Input type will be decided on the type of the left-hand side and the return type of the method is decided from the method.

Built-in Functional Interfaces

You’d think we’d need a new function type to represent this sort of expression. Instead, Java designers cleverly used existing interfaces with one single abstract method as the lambda’s type, known as functional interfaces. And stored them in a library called java.util.function

Java defines many types of functional interfaces, Read here:

Java Functional Interfaces are a standard where you can define your own functional interface. It’s up to you to use your own functional interfaces whenever required.

Key functional interfaces

Few of the functional interfaces provided by java is important for us. Let’s have a look.

1. Predicate

Checks a condition and returns boolean. Input can be any type.

@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}

We can use this to check a string is empty or not like this:

Predicate<String> stringPredicate= s -> s.isEmpty();
System.out.println(stringPredicate.test("nuwan"));
//Output: false

2. Function

The Function interface represents a function (method) that takes a single parameter and returns a single value. This will take type T and return type R. can do a data transformation within the method.

@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}

Following will convert an integer to a string.

Function<String,Integer> fun = s -> Integer.valueOf(s);
System.out.println(fun.apply("0001"));
//Output: 1

3. Consumer

The Consumer accepts a single argument but does not return any result:

@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}

Example

Consumer<String> consumer = o -> System.out.println(o);
consumer.accept("Use consumer interface");

4. Supplier

Returns something without using input parameters.

@FunctionalInterface
public interface Supplier<T> {
T get();
}

Example

Supplier<String> supplier = () ->  "Return Something";
System.out.println(supplier.get());

5. UnaryOperator

This takes a single parameter and returns a parameter of the same type.

UnaryOperator<String> uo = s -> s.toUpperCase();
System.out.println(uo.apply("send lowercase"));
//Output: SEND LOWERCASE

6. BinaryOperator

Takes two parameters and returns a single value. Both parameters and the return type must be of the same type.

BinaryOperator<String> bo = (s, s2) -> s+" - "+s2;
System.out.println(bo.apply("Hi","There"));
//Output: Hi - There

Two argument functions

The functions we discussed above are single input or no parameter functions, if we are to use two parameters as input we can use

  • BiPredicate
  • BiConsumer
  • BiFunction
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}

Primitive Function Specializations

Since a primitive type can’t be a generic type argument, there are versions of the Function interface for the most used primitive types double, int, long, and their combinations in argument and return types. In general it is recommended to use the more specialized form to avoid auto-boxing. For instance IntFunction<Foo> should be preferred over Function<Integer, Foo>.

Legacy Functional Interfaces

Many interfaces from previous versions of Java conform to the constraints of a FunctionalInterface, and we can use them as lambdas. Prominent examples include the Runnable and Callable interfaces that are used in concurrency APIs.

Virtual (default) methods

While it’s great that lambdas have been added to the language, there’s no point of having them if they cannot be used with existing APIs. In order to add support to absorb lambdas into our libraries, the interfaces needed to evolve. That is, we need to be able to add additional functionality to an already published API.

Until Java 8, the interfaces were abstract, you certainly cannot add implementation to it. However, Java 8 re-engineered this, calling them virtual methods. The collections API is one such example, where bringing lambdas into the equation has overhauled and enhanced the APIs.

As Java support multiple inheritance with Interfaces same default method could be found more than once and in that case compiler will be in trouble and developers need to provide their own implementation to fix the issue.

Method references

Method references are shortcuts for calling existing methods.

Consumer<String> consumer = o -> System.out.println(o);
consumer.accept("Use consumer interface");

The above lambda expression can be re-written as below using method references.

Consumer<String> stringConsumer = System.out::println;
consumer.accept("Use consumer interface");

Target types

Functional interfaces can provide a target type in multiple contexts, such as assignment context, method invocation, or cast context:

     // Assignment context
Predicate<String> p = String::isEmpty;

// Method invocation context
stream.filter(e -> e.getSize() > 10)...

// Cast context
stream.map((ToIntFunction) e -> e.getSize())...

Patterns and best practices

Developers should explore java.util.function package before creating new functional interfaces.

Use the @FunctionalInterface Annotation to avoid accidental changes to the interface so that it violates Functional interface standards.

Adding too many default methods to the interface is not a very good architectural decision, extending different functional interfaces with the same default method can be problematic.

Avoid Overloading Methods With Functional Interfaces as Parameters, We should use methods with different names to avoid collisions.

Keep Lambda Expressions Short and Self-explanatory, use one line constructions instead of a large block of code.

Avoid Specifying Parameter Types to Lambda, compiler, in most cases, is able to resolve the type.

//Use this 
(a, b) -> a.toLowerCase() + b.toLowerCase();
//instead of this
(String a, String b) -> a.toLowerCase() + b.toLowerCase();

Avoid Parentheses Around a Single Parameter

a -> a.toLowerCase();//YES
(a) -> a.toLowerCase();//NO

Avoid Return Statement and Braces

a -> a.toLowerCase();//Yes
a -> {return a.toLowerCase()};//No

Use Method References, when it’s possible

a -> a.toLowerCase();//No
String::toLowerCase;//Yes

Effectively Final

The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.

Use “Effectively Final” Variables, else it will cause a compile-time error. This approach makes lambda execution thread-safe.

Even though variables can’t be changed, variables inside can be changed, so when doing parallel computing with Lambdas

//compile time error
public void method() {
String localVariable = "Local";
Foo foo = parameter -> {
String localVariable = parameter;
return localVariable;
};
}

But this is possible

int[] total = new int[1];
Runnable r = () -> total[0]++;
r.run();

Reference:

https://www.oreilly.com/content/whats-new-in-java-8-lambdas/
https://www.oreilly.com/content/java-8-functional-interfaces/
https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
https://www.baeldung.com/java-8-lambda-expressions-tips

--

--

Nuwan Zen
CodeX
Writer for

Sometimes A software Engineer, sometimes a support engineer, sometimes a devops engineer, sometimes a cloud engineer :D That’s how the this life goes!