Java 8 Primitive Functional Interfaces Tutorial

Introduction

This tutorial explains Java 8's primitive functional interfaces - IntPredicate, ToIntFunction, IntUnaryOperator, ObjIntConsumer and the rest. Most of the 43 interfaces in java.util.function are of this kind, and they are why IntStream, LongStream and DoubleStream exist. We will start with the problem they solve - boxing - and the naming scheme that makes all of them easy to read. We will then run an example using each family, and finish with practical advice on when to use them.

(This tutorial assumes you know the generic interfaces covered in the java.util.function package tutorialClick to read: java.util.function package tutorial.)

The problem: generics and boxing

Java generics only work with reference types, so Function<int, int> is not allowed; you have to write Function<Integer, Integer>. Every call then boxes its int argument into an Integer objectClick to read: Integer object, unboxes it to do the arithmetic, and boxes the result again. For values outside the small cached range (-128 to 127), each box is a new object. In a stream of a million numbers, that is millions of short-lived objects and a lot of extra work for the garbage collector.
Boxing in Function of Integer versus plain int arithmetic in Java 8 IntUnaryOperator
The primitive functional interfaces take and return int, long or double directly. Their abstract methods have different names - applyAsInt(), test(int), getAsDouble() - so that a primitive value never has to pass through an Object parameter.

How to read their names

There are a lot of them, but the names follow a few simple rules. Once you know the rules, you can tell what any of them does from its name.
Naming rules of Java 8 primitive functional interfaces - IntXxx, ToIntXxx, IntToLongXxx, operators and ObjIntConsumer

Java 8 code example of primitive functional interfaces

The example uses a small Employee class with a name and a salary.
Employee.java
//Employee.java
public class Employee {
  private final String name;
  private final double salary;

  public Employee(String name, double salary) {
    this.name = name;
    this.salary = salary;
  }

  public String getName() { return name; }
  public double getSalary() { return salary; }

  @Override
  public String toString() { return name + " (" + salary + ")"; }
}
Java 8 code showing IntPredicate, IntUnaryOperator, ToIntFunction, IntFunction, IntSupplier and ObjIntConsumer
//PrimitiveInterfaces.java
import java.util.Arrays;
import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.DoubleBinaryOperator;
import java.util.function.Function;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.IntFunction;
import java.util.function.IntPredicate;
import java.util.function.IntSupplier;
import java.util.function.IntToDoubleFunction;
import java.util.function.IntUnaryOperator;
import java.util.function.ObjIntConsumer;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.stream.IntStream;

public class PrimitiveInterfaces {

  public static void main(String[] args) {
    // 1. The generic interfaces box: each int becomes an Integer object
    Function<Integer, Integer> plusOneBoxed = n -> n + 1;
    Integer a = plusOneBoxed.apply(999);
    Integer b = plusOneBoxed.apply(999);
    System.out.println("1. boxed results equal? " + a.equals(b)
        + ", same object? " + (a == b));

    // 2. The primitive versions take and return plain ints
    IntUnaryOperator plusOne = n -> n + 1;
    IntPredicate isEven = n -> n % 2 == 0;
    IntBinaryOperator larger = Math::max;
    System.out.println("2. plusOne(999) = " + plusOne.applyAsInt(999)
        + ", isEven(42) = " + isEven.test(42)
        + ", larger(7, 12) = " + larger.applyAsInt(7, 12));

    // 3. IntStream methods are declared with primitive interfaces
    int sumOfEvenSquares = IntStream.rangeClosed(1, 10)
        .filter(isEven)                 // IntPredicate
        .map(n -> n * n)                // IntUnaryOperator
        .sum();
    System.out.println("3. sum of even squares 1..10 = " + sumOfEvenSquares);

    // 4. To and from objects: ToIntFunction, ToDoubleFunction and IntFunction
    List<Employee> staff = Arrays.asList(new Employee("Tom Jones", 5200),
        new Employee("Harry Major", 4100), new Employee("Nancy Smith", 5800));
    ToIntFunction<Employee> nameLength = e -> e.getName().length();
    ToDoubleFunction<Employee> salary = Employee::getSalary;
    IntFunction<String> stars = n -> new String(new char[n]).replace('\0', '*');
    System.out.printf("4. total name length = %d, average salary = %.2f, stars(5) = %s%n",
        staff.stream().mapToInt(nameLength).sum(),
        staff.stream().mapToDouble(salary).average().getAsDouble(),
        stars.apply(5));

    // 5. Between primitive types: IntToDoubleFunction, DoubleBinaryOperator
    IntToDoubleFunction daysToWeeks = days -> days / 5.0;
    DoubleBinaryOperator weightedAverage = (x, y) -> x * 0.7 + y * 0.3;
    System.out.println("5. 12 days = " + daysToWeeks.applyAsDouble(12) + " weeks, weighted = "
        + weightedAverage.applyAsDouble(80, 60));

    // 6. Suppliers and consumers of primitives
    int[] counter = {0};
    IntSupplier nextTicket = () -> ++counter[0];
    IntConsumer printTicket = n -> System.out.print("#" + n + " ");
    BooleanSupplier ticketsLeft = () -> counter[0] < 3;
    System.out.print("6. ");
    while (ticketsLeft.getAsBoolean()) {
      printTicket.accept(nextTicket.getAsInt());
    }
    System.out.println();

    // 7. ObjIntConsumer: an object and an int, without boxing the int
    ObjIntConsumer<String> repeat = (text, times) -> {
      for (int i = 0; i < times; i++) System.out.print(text);
    };
    System.out.print("7. ");
    repeat.accept("ab", 3);
    System.out.println();
  }
}
 OUTPUT of the above code
1. boxed results equal? true, same object? false
2. plusOne(999) = 1000, isEven(42) = true, larger(7, 12) = 12
3. sum of even squares 1..10 = 220
4. total name length = 31, average salary = 5033.33, stars(5) = *****
5. 12 days = 2.4 weeks, weighted = 74.0
6. #1 #2 #3
7. ababab

Explanation of the code

  • Part 1: two calls of the boxed Function with the same input give equal Integers that are different objects - proof that each call allocated a new box. (It is also why == must never be used to compare Integers.)
  • Part 2: IntUnaryOperator, IntPredicate and IntBinaryOperator work on plain ints. Note the method names: applyAsInt() and test(). Math::max matches IntBinaryOperator because Math.max(int, int) takes and returns ints.
  • Part 3: IntStream's methods are declared with primitive interfaces: filter() takes an IntPredicate and map() an IntUnaryOperator, so the whole pipeline runs without a single Integer object. See IntStream.range()Click to read: IntStream.range().
  • Part 4: mapToInt() takes a ToIntFunction and mapToDouble() a ToDoubleFunction; both turn a stream of objects into a primitive stream, which has sum() and average(). IntFunction<String> goes the other way, from an int to an object.
  • Part 5: IntToDoubleFunction converts between primitive types, and DoubleBinaryOperator combines two doubles.
  • Part 6: IntSupplier, IntConsumer and BooleanSupplier produce and consume primitives. The counter is a one-element array because a lambda can only use effectively finalClick to read tutorial: Java 8 Lambdas: Effectively Final, Capture and this local variables; the array reference stays the same while its content changes.
  • Part 7: ObjIntConsumer takes an object and an int - the shape of an accumulator that adds a number to a container.

When to use them

  • Number crunching in streams: use mapToInt(), mapToLong() or mapToDouble() as early as possible in a numeric pipeline, then sum(), average() or summaryStatistics(). The summarizing collectorsClick to read: summarizing collectors use ToIntFunction and its relatives for the same reason.
  • Your own APIs: if a method takes a function that works on numbers and may be called millions of times, declare the parameter as IntUnaryOperator rather than Function<Integer, Integer>.
  • Back to objects: boxed() turns an IntStream into a Stream<Integer>, and mapToObj() takes an IntFunction - use them when you need to collect into a List.
  • Do not over-optimise: for a few hundred values, the generic interfaces are perfectly fine. Choose the primitive ones where the volume is high or where the API already uses them.
  • Missing types: there are no versions for byte, short, char or float. Use the int or double versions; String.chars(), for example, returns an IntStream.

Summary

In this tutorial we first saw why primitive functional interfaces exist: generic interfaces such as Function<Integer, Integer> box every value, while IntUnaryOperator and its relatives work on plain int, long and double values. Next we learned the naming rules - IntXxx, ToIntXxx, IntToLongXxx, the operators and ObjIntConsumer. We then ran an example using each family, including how IntStream and mapToInt() rely on them. Lastly, we listed when to use them and when the generic versions are fine.