Java SE 8/11 Programmer II Exam Series: Built-in Functional Interfaces

Introduction

Functional interfaces are the basics of functional programming in Java 8. If you master the six basic interfaces, you can derive the rest and understand the streams. In this section, we will cover the common functional interfaces in java.util.function package: Supplier, Consumer, Predicate, Function, UnaryOperator, and BinaryOperator.

Class Hierarchy

The diagram shows the class hierarchy of the common functional interfaces and their functional methods. It is essential to know them by heart.

Supplier

Supplier is a functional interface whose functional method is T get(). You use it when you want to produce a value or construct a new object. There is no requirement that a new or distinct result be returned each time the supplier is invoked.

public class Main {
   public static void main(String[] args) {
	List<String> supplements = Arrays.asList("bcaa", "creatin", "fish oil", "vitamin C");	
	supplements.stream().forEach(supplement -> {
		printSupplements(() -> supplement);
	});
   }
   private static void printSupplements(Supplier<String> supplier){
        System.out.println(supplier.get());
   }
}

Consumer and BiConsumer

Consumer represents an operation that accepts a single input argument and returns no result. This is a functional interface whose functional method is void accept(T t). A common example of such an operation is printing where an object is taken as input to the printing function and the value of the object is printed.

public class Main {
   public static void main(String[] args) {
	Consumer<String> consumer = Main::printFoods;
  	consumer.accept("beef");
  	consumer.accept("brown rice");
  	consumer.accept("salad");
   }
   private static void printFoods(String food){
   	System.out.println(food);
   }
} 

BiConsumer is similar to Consumer but it accepts two input arguments. Its functional method is void accept(T t, U u).

BiConsumer<String, String> b1 = (x, y) -> System.out.println(x + y);
b1.accept("1", "2");

Function and BiFunction

Function represents a function that accepts one argument and produces a result. This is a functional interface whose functional method is T apply(T t). The examples below take a String parameter and return the integer value. Note that you can both use a lambda and a method reference.

//methods reference
Function<String, Integer> f = Integer::parseInt;
System.out.println(f.apply("2"));
        
//lambda
Function<String, Integer> f2 = (x) -> Integer.parseInt(x);
System.out.println(f2.apply("2"));
        
//returns the length of the string
Function<String, Integer> f3 = String::length;
System.out.println(f2.apply("my string"));

BiFunction represents a function that accepts two arguments and produces a result. Its functional method is T apply(T t, U u).

BiFunction<String, CharSequence, Boolean> contains = String::contains;
BiFunction<String, CharSequence, Boolean> contains2 = (string, charSequence) -> string.contains(charSequence);

System.out.println(contains.apply("Suleyman", "man"));
System.out.println(contains2.apply("Suleyman", "man"));

Predicate and BiPredicate

Predicate represents a predicate (boolean-valued function) of one argument. Its functional method is boolean test(T t). Predicates are frequently used for filtering or matching. Here is an example of a simple predicate.

Predicate<String> p2 = String::isEmpty;
System.out.println(p2.test(""));

Predicate<String> p1 = (str) -> str.contains("s");
System.out.println(p1.test("Suleyman"));

A BiPredicate is the same as the Predicate except that it takes two parameters, boolean test(T t, U u).

BiPredicate<String, String> b1 = String::equalsIgnoreCase;
System.out.println(b1.test("my string", "my string"));

BiPredicate<String, String> b2 = (string, anotherString) -> string.equalsIgnoreCase(anotherString);
System.out.println(b2.test("my string", "your string"));

It is also possible to combine predicates.

Predicate<Integer> predicate1 = number -> number.intValue() < 10;
Predicate<Integer> predicate2 = number -> number.intValue() % 2 == 0;

List<Integer> and = numbers.stream()
		.filter(predicate1.and(predicate2))
		.collect(Collectors.toList());
System.out.println("Predicate - Predicate.and(): " + and);

List<Integer> or = numbers.stream()
		.filter(predicate1.or(predicate2))
		.collect(Collectors.toList());
System.out.println("Predicate - Predicate.or(): " + or);

List<Integer> negate = numbers.stream()
		.filter(predicate1.negate())
		.collect(Collectors.toList());
System.out.println("Predicate - Predicate.negate(): " + negate);

UnaryOperator and BinaryOperator

The Operator interfaces represent functions whose result and argument types are the same (Bloch, Joshua. Java (p. 361)). The UnaryOperator interface receives a single argument and returns the same value. Its functional method is R apply(T t). For instance, converting the characters in a string to lower case is a unary operation.

UnaryOperator<String> un = String::toLowerCase;
System.out.println(un.apply("UNARY"));

List<String> burhan = Arrays.asList("Burhan", "Altinop", "Idare Muduru");
burhan.replaceAll(un);

BinaryOperator represents an operation upon two operands of the same type, producing a result of the same type as the operands. Its functional method is R apply(T t, U u).

BinaryOperator<Integer> operator = (x,y) -> x + y;
System.out.println(operator.apply(5, 10));

BinaryOperator<Integer> bi = BinaryOperator.minBy(Comparator.reverseOrder());
System.out.println(bi.apply(2, 3));

BinaryOperator<Integer> bOpertorMax = BinaryOperator.maxBy((Integer t, Integer u) -> t.compareTo(u));
System.out.println(bOpertorMax.apply(10,20));

Summary

In this section, we covered the common functional interfaces: Supplier, Consumer, Predicate, Function, UnaryOperator, and BinaryOperator. We learned that they are the building blocks of stream operations in Java 8. You can find the source code on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *