Functional Interfaces Overview

Java 8 provided a lot of new and very useful features that we can use in our everyday life. In this post I decided make an overview with examples of most  commonly used built-in functional interfaces.

lambda

I’d like to start from the definition, what is the functional interface? By the definition any interface that has only one abstract method is considered to be a functional interface. Another name for such interfaces is Single Abstract Method interfaces (SAM Interfaces). For instance, Runnable is absolutely legal FI and as you can see it’s annotated now with FunctionalInterface annotation.

The annotation is not obligatory, but it’s better to use it because it will help you to identify not valid functional interfaces during compilation.

Interfaces

The build-in interfaces are available for you in the java.util.function package.

Interface Return Type Method Name Parameters
Predicate boolean test T t
BiPredicate boolean test T t, U u
Consumer void accept T t
BiConsumer void accept T t, U u
Supplier T get -
Function R apply T t
BiFunction R apply T t, U u
UnaryOperator T apply T t
BinaryOperator T apply T t1, T t2

Predicate and BiPredicate

Predicate accepts only one parameter and returns boolean value. So it’s used for checking some condition, for example it’s widely used in Stream filtering and matching.

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

Examples:

Predicate<String> isHttp = (s) -> s.startsWith("http");
System.out.println(isHttp.test("http://www.alexvolov.com")); // true

Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)
    .stream()
    .filter((e) -> e % 2 == 0)
    .forEach(System.out::print); // 2468

// Default methods
Predicate<String> hasTomato = (s) -> s.contains("tomato");
Predicate<String> hasMozzarella = (s) -> s.contains("mozzarella");
Predicate<String> hasTomatoAndMozzarella = hasTomato.and(hasMozzarella);

System.out.println(hasTomatoAndMozzarella.test("tomato, mozzarella, basil")); // true

The difference between Predicate and BiPredicate is that the last accepts two parameters instead of one.

Consumer and BiConsumer

As it name suggests it represents an operation that takes an argument but doesn’t return any result.

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

Examples: 

Consumer<Integer> print = (s) -> System.out.print(s);

print.accept(1_000_000); // 1000000

Arrays.asList(1, 2, 3).stream().forEach(print); // 123

// Default methods

System.out.println();

Consumer<String> printLength = 
        (s) -> System.out.print("Length: " + s.length() + "; ");
Consumer<String> printUpper = 
        (s) -> System.out.print("Upper: " + s.toUpperCase() + "; ");
Consumer<String> printLower = 
        (s) -> System.out.print("Lower: " + s.toLowerCase() + "; ");

printLength
        .andThen(printUpper)
        .andThen(printLower)
        .accept("Word"); // Length: 4; Upper: WORD; Lower: word; 

The difference between Consumer and BiConsumer is that the last accepts two parameters instead of one.

Supplier

Use Supplier when you need to get a result without any input. For instance, it can return current date or new instance of an object.

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

Examples:

Supplier<ExecutorService> newCachedThreadPool = () -> {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
};

ExecutorService cachedPool = newCachedThreadPool.get();

cachedPool.submit(() -> System.out.println("Hello"));

cachedPool.shutdown();
        

Function and BiFunction

Function accepts a parameter of one type and can return result of another type. BiFunction works at the same way but accepts two parameters.

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

Examples:

Function<String, Integer> hashcode = (value -> {
    int h = 0;
    if (h == 0 && value.length() > 0) {
        for (int i = 0; i < value.length(); i++) {
            h = 31 * h + value.charAt(i);
        }
    }
    return h;
});

System.out.println(hashcode.apply("Maria")); // 74113750

// Default methods
Function<Integer, Integer> divide = (v) -> v / 2;
Function<Integer, Integer> addTwo = (v) -> v + 2;

System.out.println(divide.andThen(addTwo).apply(100)); // 52
System.out.println(divide.compose(addTwo).apply(50)); // 26

The difference between Function and BiFunction is that the last accepts two parameters instead of one. If you need three parameters or more feel free to create your own functional interfaces.

UnaryOperator and BinaryOperator

UnaryOperator extends Function and BinaryOperator extends BiFunction because both are special cases of Function. The difference is that UnaryOperator accepts and returns the same type.

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
}

Examples:

UnaryOperator<Integer> increment = (v) -> ++v;
System.out.println(increment.apply(100)); // 101

The difference between UnaryOperator and BinaryOperator is that the last accepts two parameters instead of one.

Leave a Reply