A Functional Interface in Java is an interface that has exactly one abstract method. Apart from this abstract method, it can include default and static methods. Java 8 introduced the @FunctionalInterface
annotation to ensure an interface follows the rules of Functional Interface. It’s optional but good practice to use this annotation.
Functional interfaces are extensively used in Java’s lambda expressions. The main purpose of a functional interface is to be used as Lambda Expressions or Method References.
Here’s a basic example of defining a functional interface:
@FunctionalInterface
interface GreetingService {
void sayMessage(String message);
}
You could use it in conjunction with a lambda like this:
GreetingService greetService = message -> System.out.println("Hello " + message);
greetService.sayMessage("world");
In the code above, message -> System.out.println("Hello " + message)
is a lambda expression that provides the implementation of the abstract method sayMessage(String message)
.
Java 8 has also defined several built-in functional interfaces. These built-in interfaces are packed in the java.util.function
package. Some common ones include Predicate<T>
, Function<T, R>
, Supplier<T>
, and Consumer<T>
. Furthermore, BinaryOperator<T>
, UnaryOperator<T>
, BiFunction<T, U, R>
are some other standard functional interfaces available.
Here are some examples of Java 8 built-in functional interfaces:
1. Predicate
Predicate<T>
is a functional interface that takes a single input and returns a boolean value. It is located in java.util.function
package.
Predicate<String> lengthCheck = s -> s.length() > 5;
System.out.println(lengthCheck.test("Hello")); // Output: false
Predicate is often used when you need to pass some sort of condition or filter as a parameter. For example, you might be checking if the User inputs are valid:
Predicate<String> isValidEmail = email -> email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
System.out.println(isValidEmail.test("[email protected]")); // Output: true
System.out.println(isValidEmail.test("testgmail.com")); // Output: false
2. Function
Function<T, R>
is an interface that accepts one argument and produces a result.
Function<String, Integer> parse = Integer::parseInt;
System.out.println(parse.apply("123")); // Output: 123
A Function<T, R>
can be useful when you need to convert from one type to another, such as transforming a list of String
into a list of Integer
.
Function<String, Integer> stringToInteger = Integer::parseInt;
List<String> strings = Arrays.asList("1", "2", "3");
List<Integer> integers = strings.stream()
.map(stringToInteger)
.collect(Collectors.toList());
3. Consumer
Consumer<T>
is an interface that takes one argument and returns no results. It is meant for implementing side effects.
Consumer<String> printer = System.out::println;
printer.accept("Hello"); // Output: Hello
The Consumer<T>
interface is often used in conjunction with Java streams or Optional, where you have a collection of objects, and you want to perform a certain action on each of the objects.
Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
List<String> names = Arrays.asList("Jon", "Sansa", "Arya", "Bran");
names.forEach(printUpperCase);
4. Supplier
Supplier<T>
is an interface that does not take any argument, but it produces a result.
Supplier<LocalDate> current = LocalDate::now;
System.out.println(current.get()); // Output: [current date]
Suppose you have a class RandomService that produces random numbers and is supposed to be used by other classes in your system.
class RandomService {
Supplier<Double> getRandomNumber = Math::random;
}
// Usage in another class
RandomService rs = new RandomService();
System.out.println(rs.getRandomNumber.get());
5. BinaryOperator and UnaryOperator
UnaryOperator<T>
takes one argument and returns a result of the same type. BinaryOperator<T>
takes two arguments and returns a result of the same type.
UnaryOperator<String> upperifier = String::toUpperCase;
System.out.println(upperifier.apply("hello")); // Output: HELLO
BinaryOperator<String> concatenator = String::concat;
System.out.println(concatenator.apply("Hello ", "World")); // Output: Hello World
You have already learned about a few key functional interfaces in Java and how to use them with lambda expressions. Now, I’ll introduce you to a few more advanced topics about functional interfaces:
1. Custom Functional Interface
If the built-in functional interfaces in Java do not satisfy your requirements, you can define your own functional interfaces. Here is an example of a custom functional interface:
@FunctionalInterface
interface CustomInterface {
String concatenateStrings(String s1, String s2);
}
You can now use it like this:
CustomInterface ci = (s1, s2) -> s1 + s2;
System.out.println(ci.concatenateStrings("Hello", " World")); // Output: Hello World
2. Method References
In some cases, lambdas just call an existing method. In those cases, we can use method references to make the code clearer. Here are some examples
Consumer<String> printer = System.out::println; // same as s -> System.out.println(s)
Predicate<String> lengthCheck = String::isEmpty; // same as s -> s.isEmpty()
Supplier<LocalDate> current = LocalDate::now; // same as () -> LocalDate.now()
3. Chaining Functional Interface Calls (Compose and AndThen)
You can chain multiple calls of Function
, Consumer
, and Predicate
using default methods they provide, such as compose
, andThen
.
Function<Integer, Integer> multiplyBy2 = x -> x * 2;
Function<Integer, Integer> add1 = x -> x + 1;
Function<Integer, Integer> add1AndThenMultiplyBy2 = add1.andThen(multiplyBy2);
System.out.println(add1AndThenMultiplyBy2.apply(2)); // Output: 6
Remember that with compose
, functions execute in reverse order.
Function<Integer, Integer> multiplyBy2ThenAdd1 = add1.compose(multiplyBy2);
System.out.println(multiplyBy2ThenAdd1.apply(2)); // Output: 5
Chaining calls this way leads to functional-style programming that can make your code more readable and maintainable by creating pipelines of transformations.
Real-world use of the functional interface is prevalent in Java library features such as Stream API, where they come together with lambda expressions to offer functional programming capabilities. They help contribute to writing clean, robust and concurrent code structures.
Overall, functional interfaces bring the power of functional programming to Java and are extensively used for implementing simple callback-style interfaces, or for defining “thin” data structures used in control statements, among other uses.