Java Basics: Lambda Built-in Functional Interfaces

Built-in Functional Interfaces

In Java 8, there are a lot of method signatures that refer to interfaces in java.util.function. Therefore, it is important to understand what these interfaces do and what variations on the basics exist. It makes writing lambda expressions a lot easier.

The java.util.function Package

  • Predicate: An expression that returns a boolean
  • Consumer: An expression that performs operations on an object passed as argument and has a void return type
  • Function: Transforms a T to a U
  • Supplier: Provides an instance of a T (such as a factory)
  • Primitive variations
  • Binary variations

Predicate is not the only functional interface provided with Java. A number of standard interfaces are designed as a starter set for developers.

Example Assumptions

The following two declarations are assumed for the examples that follow:

14 List[SalesTxn] tList = SalesTxn.createTxnList();
15 SalesTxn first = tList.get(0);

One or both of the declarations pictured are assumed in the examples that follow.

Predicate

A Predicate takes a generic class and returns a boolean. It has a single method, namely test.

1 package java.util.function;
2 
3 public interface Predicate[T] {
4    public boolean test(T t);
5 }
6

Predicate: Example

In this example, a SalesTxn is tested to see if it was executed in the state of MA. The filter method takes a predicate as a parameter. In the second example (starting on line 24), notice that the predicate can call its test method with a SalesTxn as a parameter. This is a clear example of taking the generic value and returning a boolean.

16  Predicate[SalesTxn] massSales = 
17    t -> t.getState().equals(State.MA);
18 
19   System.out.println("\n== Sales - Stream");
20   tList.stream()
21     .filter(massSales)
22     .forEach(t -> t.printSummary());
23 
24  System.out.println("\n== Sales - Method Call"); 
25  for(SalesTxn t:tList){
26    if (massSales.test(t)){
27      t.printSummary();
28    }
29  }

Consumer

A Consumer takes a generic and returns nothing. It has a single method accept.

1 package java.util.function;
2 
3 public interface Consumer[T] {
4 
5      public void accept(T t);
6
7 }

Consumer: Example

Note how the Consumer is defined and nothing is returned. The example takes a sales transaction and prints a couple values.

17   Consumer[SalesTxn] buyerConsumer = t ->
18     System.out.println("Id: " + t.getTxnId()
19        + " Buyer: " + t.getBuyer().getName());
20 
21   System.out.println("== Buyers - Lambda");
22   tList.stream().forEach(buyerConsumer);
23 
24   System.out.println("== First Buyer - Method");
25   buyerConsumer.accept(first);

Two examples are provided above. The first shows that the default parameter for forEach is Consumer. The second shows that once a lambda expression is stored, it can be executed on the specified type by using the accept method.

Function

A Function takes one generic type and returns another. Notice that the input type comes first in the list and then the return type. So the apply method takes a T and returns an R.

1 package java.util.function;
2 
3 public interface Function[T,R] {
4 
5   public R apply(T t);
6 }
7}

Function: Example

The example takes a SalesTxn and returns a String. The Function interface is used frequently in the update Collection APIs.

17  Function[SalesTxn, String] buyerFunction = 
18   t -> t.getBuyer().getName();
19 
20   System.out.println("\n== First Buyer");
21   System.out.println(buyerFunction.apply(first)); 
22 }

Supplier

The Supplier returns a generic type and takes no parameters.

1 package java.util.function;
2 
3 public interface Supplier[T] {
4 
5    public T get();
6 }
7

Supplier: Example

In the example, the Supplier creates a new SalesTxn. On line 31, notice that calling get generates a SalesTxn from the lambda that was defined earlier.

15   List[SalesTxn] tList = SalesTxn.createTxnList();
16   Supplier[SalesTxn] txnSupplier = 
17     () -> new SalesTxn.Builder()
18       .txnId(101)
19       .salesPerson("John Adams")
20       .buyer(Buyer.getBuyerMap().get("PriceCo"))
21       .product("Widget")
22       .paymentType("Cash")
23       .unitPrice(20)
//... Lines ommited
29      .build();
30 
31    tList.add(txnSupplier.get());
32    System.out.println("\n== TList");
33    tList.stream().forEach(SalesTxn::printSummary);

Primitive Interface

If you look at the API docs, there are a number of primitive interfaces that mirror the main types: Predicate, Consumer, Function, Supplier. These are provided to avoid the negative performance consequences of auto-boxing and unboxing.

Return a Primitive Type

The ToDoubleFunction interface takes a generic type and returns a double.

1  package java.util.function;
2 
3  public interface ToDoubleFunction[T] {
4 
5     public double applyAsDouble(T t);
6  }

Return a Primitive Type: Example

This example calculates a value from a transaction and returns a double. Notice that the method name changes a little, but this is still a Function. Pass in one type and return something else, in this case a double.

18   ToDoubleFunction[SalesTxn] discountFunction = 
19     t -> t.getTransactionTotal()
20       * t.getDiscountRate();
21 
22   System.out.println("\n== Discount");
23   System.out.println(
24     discountFunction.applyAsDouble(first));

Process a Primitive Type

Notice that a DoubleFunction specifies only one generic type, but a Function takes two. The apply method takes a double and returns the generic type. So the double, in this case, is the input and the generic type is the output.

1 package java.util.function;
2 
3 public interface DoubleFunction[R] {
4 
5    public R apply(double value);
6 }
7}

Process Primitive Type: Example

The example computes a value and then returns the result as a String. The value is passed in on line 14.

9    A06DoubleFunction test = new A06DoubleFunction();
10 
11   DoubleFunction[String] calc = 
12     t -> String.valueOf(t * 3);
13 
14   String result = calc.apply(20);
15   System.out.println("New value is: " + result);

Binary Types

The binary version of the standard interfaces allows two generic types as input. In this example, the BiPredicate takes two parameters and returns a boolean.

1 package java.util.function;
2 
3 public interface BiPredicate[T, U] {
4 
5    public boolean test(T t, U u);
6 }
7

Binary Type: Example

This example takes a SalesTxn and a String to do a comparison and return a result. The test method merely takes two parameters instead of one.

14   List[SalesTxn] tList = SalesTxn.createTxnList();
15   SalesTxn first = tList.get(0); 
16   String testState = "CA";
17 
18   BiPredicate[SalesTxn,String] stateBiPred = 
19     (t, s) -> t.getState().getStr().equals(s);
20 
21   System.out.println("\n== First is CA?");
22   System.out.println(
23     stateBiPred.test(first, testState))

Unary Operator

The UnaryOperator takes a class as input and returns an object of the same class.

1  package java.util.function;
2 
3  public interface UnaryOperator[T] extends Function[T,T] {
4    @Override
5    public T apply(T t);
6  }

UnaryOperator: Example

If you need to pass in something and return the same type, use the UnaryOperator interface. The UnaryOperator interface takes a generic type and returns that same type. This example takes a String and returns the String in uppercase.

17 UnaryOperator[String] unaryStr = 
18   s -> s.toUpperCase();
19 
20   System.out.println("== Upper Buyer");
21   System.out.println(
22     unaryStr.apply(first.getBuyer().getName()));

Wildcard Generics Review

When using the built-in functional interfaces, generic wildcard statements are used frequently. The two most common wildcards you will see are listed below.

  • ? super T – This class and any of its super types
  • ? extends T – This class and any of its subtypes
Related Post