Photo by Tracy Adams on Unsplash
Table of Contents
Introduction
Java 8 introduced lambda expressions and functional interfaces. It introduced a powerful set of functional interfaces in the java.util.function
package, enhancing the language’s capabilities for functional programming.
Among these, Predicate
, Consumer
, Supplier
, and Function
stand out for their versatility and widespread application. This blog post dives deep into each of these interfaces, providing insights and examples to demonstrate their utility.
What are Functional Interfaces in Java 8?
In Java 8, a Functional Interface is an interface that has only one abstract method. It can have any number of default and static methods, but it must have only one abstract method.
The purpose of introducing Functional Interfaces in Java 8 is to support Lambda expressions and method references.
Here are some key points about Functional Interfaces in Java 8:
Lambda Expressions: Functional Interfaces are designed to be used with Lambda expressions, which allow you to represent an instance of the interface in a concise way. Lambda expressions provide an easy way to implement the single abstract method defined by the Functional Interface.
@FunctionalInterface Annotation: Java provides the @FunctionalInterface
annotation. The primary reason to use a functional interface is to enforce having a single abstract method.
If someone by mistake adds a second abstract method compiler will raise the interface violates the rules of a Functional Interface (e.g., having more than one abstract method), and the compiler will generate an error.
@FunctionalInterface
interface InvalidInterface {
int operation(int a, int b);
// Attempting to add another abstract method:
String formatResult(int result);
}
//Compiler Error
/*
java: Unexpected @FunctionalInterface annotation
lambda_expression.consumer.InvalidInterface is not a functional interface
multiple non-overriding abstract methods found in interface lambda_expression.consumer.InvalidInterface
*/
Built-in Functional Interfaces: Java 8 introduced several built-in Functional Interfaces in the java.util.function
package, such as Consumer
, Predicate
, Function
, Supplier
, and more. These interfaces are commonly used with Lambda expressions and stream operations.
Custom Functional Interfaces: You can also define your own custom Functional Interfaces to suit your specific requirements.
Method References: In addition to Lambda expressions, Functional Interfaces also support method references, which provide a more concise way to refer to an existing method implementation.
Here’s an example of a custom Functional Interface and how it can be used with a Lambda expression:
@FunctionalInterface
interface Operation {
int perform(int a, int b);
}
public class FunctionalInterfaceExample {
public static void main(String[] args) {
Operation addition = (a, b) -> a + b;
int result = addition.perform(5, 3); // result = 8
System.out.println("Result: " + result);
}
}
In this example, Operation
is a custom Functional Interface with a single abstract method perform
. We create a Lambda expression (a, b) -> a + b
and assign it to an instance of the Operation
interface. This Lambda expression implements the perform
method and can be used to perform addition.
Functional Interfaces and Lambda expressions are essential features introduced in Java 8 that enable functional programming style and promote more concise and expressive code.
Benefits of Using Functional Interfaces and Lambda Expressions:
- Conciseness: Lambda expressions significantly reduce code size compared to traditional anonymous inner classes.
- Readability: Their clear and compact syntax improves code comprehension, especially for short, well-defined functions.
- Improved Functional Programming: They enable a more functional programming style, promoting code clarity and maintainability.
- Stream API Integration: Seamlessly integrates with the Java Stream API, allowing for powerful data manipulation and processing.
Predicate
A Predicate<T>
represents a single argument function that returns a boolean value. It’s primarily used for filtering or matching.
@FunctionalInterface
public interface Predicate<T> {
/**
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
/**
* Returns a composed predicate that represents a short-circuiting logical
* AND of this predicate and another. When evaluating the composed
* predicate, if this predicate is {@code false}, then the {@code other}
* predicate is not evaluated.
*
* <p>Any exceptions thrown during evaluation of either predicate are relayed
* to the caller; if evaluation of this predicate throws an exception, the
* {@code other} predicate will not be evaluated.
*
* @param other a predicate that will be logically-ANDed with this
* predicate
* @return a composed predicate that represents the short-circuiting logical
* AND of this predicate and the {@code other} predicate
* @throws NullPointerException if other is null
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
/**
* Returns a predicate that represents the logical negation of this
* predicate.
*
* @return a predicate that represents the logical negation of this
* predicate
*/
default Predicate<T> negate() {
return (t) -> !test(t);
}
/**
* Returns a composed predicate that represents a short-circuiting logical
* OR of this predicate and another. When evaluating the composed
* predicate, if this predicate is {@code true}, then the {@code other}
* predicate is not evaluated.
*
* <p>Any exceptions thrown during evaluation of either predicate are relayed
* to the caller; if evaluation of this predicate throws an exception, the
* {@code other} predicate will not be evaluated.
*
* @param other a predicate that will be logically-ORed with this
* predicate
* @return a composed predicate that represents the short-circuiting logical
* OR of this predicate and the {@code other} predicate
* @throws NullPointerException if other is null
*/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
/**
* Returns a predicate that tests if two arguments are equal according
* to {@link Objects#equals(Object, Object)}.
*
* @param <T> the type of arguments to the predicate
* @param targetRef the object reference with which to compare for equality,
* which may be {@code null}
* @return a predicate that tests if two arguments are equal according
* to {@link Objects#equals(Object, Object)}
*/
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
/**
* Returns a predicate that is the negation of the supplied predicate.
* This is accomplished by returning result of the calling
* {@code target.negate()}.
*
* @param <T> the type of arguments to the specified predicate
* @param target predicate to negate
*
* @return a predicate that negates the results of the supplied
* predicate
*
* @throws NullPointerException if target is null
*
* @since 11
*/
@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}
Examples: Filtering a List
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
Predicate<String> startsWithA = name -> name.startsWith("A");
List<String> namesStartingWithA = names.stream()
.filter(startsWithA)
.collect(Collectors.toList());
System.out.println(namesStartingWithA); // [Alice]
If you check the above code, Predicate is passsing in the filter
method in Java 8’s Stream. API has the following signature:
Stream<T> filter(Predicate<? super T> predicate)
Combining Predicates
Predicate<String> longerThanThree = name -> name.length() > 3;
Predicate<String> startsWithC = name -> name.startsWith("C");
List<String> filteredNames = names.stream()
.filter(longerThanThree.and(startsWithC))
.collect(Collectors.toList());
System.out.println(filteredNames); // [Charlie]
Consumer
The Consumer<T>
functional interface in Java 8 can only take a single input argument, and it doesn’t return any result.
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
/**
* Returns a composed {@code Consumer} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
The accept
method takes a single argument of type T
and doesn’t return any value (it has a void
return type).
Examples Printing List Elements
Consumer<String> printConsumer = System.out::println;
names.forEach(printConsumer); // Prints each name in the list
Consumer<String>: This declares a functional interface Consumer that takes a String as input and performs an operation (doesn’t return anything).
System.out::println: This is a method reference. It’s a shorthand way of creating a lambda expression that’s equivalent to: s -> System.out.println(s).
names.forEach(printConsumer): The forEach method on the list iterates over each name in the names list and applies the printConsumer (which uses the System. out:println method reference) to print each name.
Modifying List Elements
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3));
Consumer<List<Integer>> multiplyByTwo = list -> {
for (int i = 0; i < list.size(); i++) {
list.set(i, list.get(i) * 2);
}
};
multiplyByTwo.accept(numbers);
System.out.println(numbers); // [2, 4, 6]
Consumer<List<Integer>>
: AConsumer
that takes aList<Integer>
as input.list -> { ... }
: A lambda expression that defines the action: iterate over the list and multiply each element by 2.multiplyByTwo.accept(numbers)
: Executes the lambda expression, modifying the originalnumbers
list.
Supplier: What is the Supplier Interface?
The Supplier<T>
interface represents a supplier of results. It has a single method, T get()
, which returns an instance of T
. A Supplier<T>
takes no arguments but returns a result. It’s often used for lazy generation of values.
Here’s the basic structure of the Supplier<T>
interface:
package java.util.function;
/**
*
* @param <T> the type of outcomes return by supplier
*/
@FunctionalInterface
public interface Supplier<T> {
T get();
}
How to Use the Supplier Interface?
The Supplier
interface can be used whenever you need a functional interface that supplies a value of some sorts. Here’s an example:
Examples:
1. Generating Random Numbers
Supplier<Double> randomSupplier = Math::random;
System.out.println(randomSupplier.get()); // Random double between 0.0 and 1.0
2. Instantiating Objects
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> newList = listSupplier.get();
newList.add("Generated");
System.out.println(newList); // [Generated]
3. Return String
Supplier<String> stringSupplier = () -> "Hello, World!";
System.out.println(stringSupplier.get()); // prints: Hello, World!
Function
A Function<T,R>
takes one argument of type T
and returns a result of the type R
. It’s useful for transforming data.
Examples
Converting Strings to Lengths
Function<String, Integer> stringLengthFunction = String::length;
List<Integer> lengths = names.stream()
.map(stringLengthFunction)
.collect(Collectors.toList());
System.out.println(lengths); // [5, 3, 7, 5]
Concatenating Strings
Function<String, String> addExclamation = s -> s + "!";
List<String> excitedNames = names.stream()
.map(addExclamation)
.collect(Collectors.toList());
System.out.println(excitedNames); // [Alice!, Bob!, Charlie!, Diana!]
Here are examples of using BiPredicate
, BiConsumer
, and BiFunction
in Java 8:
1. BiPredicate
A BiPredicate
represents a predicate (boolean-valued function) that takes two arguments. It can be used for filtering or testing elements based on two inputs.
import java.util.function.BiPredicate;
public class BiPredicateExample {
public static void main(String[] args) {
BiPredicate<Integer, Integer> isGreaterThan = (a, b) -> a > b;
boolean result1 = isGreaterThan.test(5, 3); // true
boolean result2 = isGreaterThan.test(2, 4); // false
System.out.println(result1); // true
System.out.println(result2); // false
}
}
In this example, isGreaterThan
is a BiPredicate
that takes two Integer
values and returns true
if the first value is greater than the second value.
2. BiConsumer
A BiConsumer
represents an operation that takes two arguments and performs some action or side-effect on those arguments.
import java.util.function.BiConsumer;
public class BiConsumerExample {
public static void main(String[] args) {
BiConsumer<String, Integer> printStringAndNumber = (str, num) -> {
System.out.println("String: " + str);
System.out.println("Number: " + num);
};
printStringAndNumber.accept("Hello", 42);
}
}
This example defines a BiConsumer
called printStringAndNumber
that takes a String
and an Integer
as arguments and prints them to the console.
3. BiFunction
A BiFunction
represents a function that takes two arguments and produces a result.
import java.util.function.BiFunction;
public class BiFunctionExample {
public static void main(String[] args) {
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
int result = sum.apply(5, 3); // 8
System.out.println(result);
}
}
In this example, sum
is a BiFunction
that takes two Integer
values and returns their sum. The apply
method is used to invoke the function with the provided arguments.
These examples demonstrate how BiPredicate
, BiConsumer
, and BiFunction
can be used to work with operations that involve two arguments. They provide a more concise and functional way of representing and combining operations in Java 8.
Conclusion
Java 8’s functional interfaces bring a functional programming flair to the language, simplifying code and making it more expressive. Whether you’re filtering collections with Predicate
, performing operations with Consumer
, generating values with Supplier
, or transforming data with Function
, these interfaces offer a robust toolkit for tackling a wide array of programming challenges. Embrace these examples as a starting point, and explore the vast possibilities they unlock in your Java applications.