Functional Interfaces in Java
Disclaimer: For the best experience, read this article in its original MD format, that includes embedded code snippets and references to code examples.
Functional Programming is a programming paradigm that decomposes a problem into a set of Functions.
Java 8 introduced some major features to support Functional Programming in Java allowing Data Immutability and enabling Functions as First-class citizens, introducing a new syntax, lambda expressions, to make Functions easier to write and read.
Also, Functional Interfaces were introduced to provide meaningful semantics to functions.
What Functional Interfaces are?
A Functional Interface in Java is an Interface with a Single Abstract Method (SAM). Java ensures the integrity of these interfaces at the compiler level with a single-method rule.
They are designed to work seamlessly with lambda expressions and method references, providing Provide semantic clarity by explicitly defining the intent of a function.
Custom Functional Interfaces
We can define our own Custom Functional Interfaces annotating our interface with @FunctionalInterface. This enables the compiler to enforce SAM rule so that we provide one and only one Abstract Method to be implemented.
Let's see an example of a custom functional interface
@FunctionalInterface
interface Greeting {
void sayHello(String name);
}
This functional interface can be implemented quite easily using a lambda expression:
Greeting greet = name -> System.out.println("Hello, " + name);
Java native Functional Interfaces
Java provides several built-in functional interfaces in the java.util.function package for common use cases in Functional Programming to best categorize and reuse functions when decomposing problems:
Predicates
Predicate<T> represents a Condition.
boolean test(T t)
They are mainly used for data filtering and segmentation, which can be handy in data intelligence to identify patterns (risk analysis, fraud detection).
@Test
public void testPredicates() {
Predicate<String> isLongString = s -> s.length() > 5;
Predicate<String> startsWithF = s -> s.startsWith("f") || s.startsWith("F");
assertFalse(isLongString.test("hello"));
assertTrue(isLongString.test("functional"));
assertTrue(startsWithF.test("functional"));
// Predicates can be easily chained
assertTrue((isLongString.and(startsWithF)).test("functional"));
}
Consumer
A Consumer<T> is an operation that accepts one type T of data and produces no result.
void accept(T t)
This is a good way to isolate side effects, and can be used for persisting data, auditing, metrics collection, sending notifications...
@Test
public void testConsumers() {
Consumer<String> printMessage = s -> System.out.println("Logging: " + s);
printMessage.accept("Example log message"); // Output: "Logging: Example log message"
}
Supplier
A Supplier<T> is a function that takes no argument and produces a result of type T.
T get()
They are mainly used to generate data for testing, configuration, default values...
@Test
public void testSuppliers() {
Supplier<Double> randomValue = () -> Math.random();
// Supplier<User> getRandomUser = User::random; // Example of a class User that would have one method random to generate a random user to be used in testing.
System.out.println(randomValue.get());
}
Recommended by LinkedIn
Function<T, R>
Function<T, R> interfaces are Functions that take a type T as input parameter and returns type R as result.
R apply(T t)
They are typically used for transforming data from type T to type R, which can be handy for transforming data form one domain to another, like generating reports, or currency conversion, processing natural language...
@Test
public void testFunctions() {
Function<Integer, String> intToString = i -> "Number: " + i;
assertEquals("Number: 42", intToString.apply(42));
}
Unitary Operator
UnaryOperator<T> is a Function where the input and the output are of the same type T. It is actually an specialization of Function<T, T>.
T apply(T t)
Typically used to modify data in batches, such as updating statuses when an order has been shipped, apply taxes, data normalization.
prices.stream()
.map(price -> price * 0.9)
.collect(Collectors.toList());
BiFunction
BiFunction<T, U, R> is a Function that takes two arguments and produces a result.
R apply(T t, U u)
They are mainly used to combine data for conflict resolution, data enrichment or generating financial summaries.
@Test
public void testCompareNames() {
// BiFunction<String, String, Boolean> twoUsersHaveSameName = String::equalsIgnoreCase;
BiFunction<String, String, Boolean> twoUsersHaveSameName = (firstName, secondName) -> firstName.equalsIgnoreCase(secondName);
assertFalse(twoUsersHaveSameName.apply("John", "Peter"));
assertTrue(twoUsersHaveSameName.apply("John", "joHN"));
}
BinaryOperator
BinaryOperator<T> is a specialization of BiFunction<T, T, T>.
T apply(T t1, T t2)
An example can be the following
@Test
public void testComposeFullNames() {
BinaryOperator<String> fullNameUsingBinaryOperator = (firstName, lastName) -> lastName + ", " + firstName;
BiFunction<String, String, String> fullNameUsingBiFunction = (firstName, lastName) -> lastName + ", " + firstName;
assertEquals("Doe, John", fullNameUsingBinaryOperator.apply("John", "Doe"));
assertEquals("Doe, John", fullNameUsingBiFunction.apply("John", "Doe"));
}
More native Functional Interfaces
There are other well known Interfaces in Java that has benefit from these changes, like
Runnable task = () -> System.out.println("Task is running..."); new Thread(task).start();
- Comparator since Java 2, for comparing and sorting objects.
Comparator<String> byLength = (s1, s2) -> Integer.compare(s1.length(), s2.length()); List<String> words = List.of("apple", "banana", "cherry"); words.sort(byLength);
Callable<String> task = () -> "Task completed!";
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<String> result = executor.submit(task);
System.out.println(result.get());
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
button.addActionListener(e -> System.out.println("Button clicked!"));