You sum line quantities across a busy catalog. map plus Integer::sum compiles. It also boxes every int into an Integer on the way through Function<LineItem, Integer>:

int units = orders.stream()
        .flatMap(o -> o.items().stream())
        .map(LineItem::quantity)   // Function<LineItem, Integer> — boxes
        .reduce(0, Integer::sum);

The same pipeline with a primitive slot does not allocate that wrapper:

int units = orders.stream()
        .flatMap(o -> o.items().stream())
        .mapToInt(LineItem::quantity)   // ToIntFunction<LineItem>
        .sum();

The primitive grid exists to skip boxing on a hot numeric path, not to decorate ordinary checkout code. Money in the shared lab stays BigDecimal on Order.total(). Use quantity, count, or score when you need an int. The series hub owns the glossary; here we only catalog the remaining ~30 types and the slots that already chose them.

The same four shapes, frozen to a primitive

Function, Predicate, Consumer, and Supplier are the object versions. Operators freeze input and output to the same type. Bi-arity adds a second argument. This page is the last family: int / long / double (and one boolean supplier) so the hot path does not pay for Integer.

Read the name as a sentence:

In the nameMeans
Int / Long / Double as a prefixthat primitive is an argument
ToInt / ToLong / ToDoublethat primitive is the return
ObjInt (and ObjLong / ObjDouble)object + primitive, the int stays unboxed
applyAsInt / getAsInt / testsame job as apply / get / test, primitive result

Suppliers break the prefix rule: IntSupplier and BooleanSupplier have no argument. The prefix is what getAsInt / getAsBoolean returns.

There is no BooleanPredicate: Predicate already returns boolean. There is a BooleanSupplier, because Supplier<Boolean> would box.

Functions — object ↔ primitive, primitive ↔ primitive

TypeSAMReads as
IntFunction<R>R apply(int)int → R
LongFunction<R>R apply(long)long → R
DoubleFunction<R>R apply(double)double → R
ToIntFunction<T>int applyAsInt(T)T → int
ToLongFunction<T>long applyAsLong(T)T → long
ToDoubleFunction<T>double applyAsDouble(T)T → double
ToIntBiFunction<T,U>int applyAsInt(T, U)(T, U) → int
ToLongBiFunction<T,U>long applyAsLong(T, U)(T, U) → long
ToDoubleBiFunction<T,U>double applyAsDouble(T, U)(T, U) → double
IntToLongFunctionlong applyAsLong(int)int → long
IntToDoubleFunctiondouble applyAsDouble(int)int → double
LongToIntFunctionint applyAsInt(long)long → int
LongToDoubleFunctiondouble applyAsDouble(long)long → double
DoubleToIntFunctionint applyAsInt(double)double → int
DoubleToLongFunctionlong applyAsLong(double)double → long

ToIntFunction is the one you meet first: Stream.mapToInt. IntFunction is the way back to objects: IntStream.mapToObj. The IntToLong / DoubleToInt row is widening or narrowing between primitives — a cast, not a rounding policy.

Operators — same primitive in and out

Object UnaryOperator / BinaryOperator with the type frozen to int, long, or double.

TypeSAMReads as
IntUnaryOperatorint applyAsInt(int)int → int
LongUnaryOperatorlong applyAsLong(long)long → long
DoubleUnaryOperatordouble applyAsDouble(double)double → double
IntBinaryOperatorint applyAsInt(int, int)(int, int) → int
LongBinaryOperatorlong applyAsLong(long, long)(long, long) → long
DoubleBinaryOperatordouble applyAsDouble(double, double)(double, double) → double

IntStream.map takes the unary. IntStream.reduce takes the binary. AtomicInteger.updateAndGet / accumulateAndGet take the same pair.

Predicates, consumers, suppliers

TypeSAM
IntPredicateboolean test(int)
LongPredicateboolean test(long)
DoublePredicateboolean test(double)
IntConsumervoid accept(int)
LongConsumervoid accept(long)
DoubleConsumervoid accept(double)
ObjIntConsumer<T>void accept(T, int)
ObjLongConsumer<T>void accept(T, long)
ObjDoubleConsumer<T>void accept(T, double)
IntSupplierint getAsInt()
LongSupplierlong getAsLong()
DoubleSupplierdouble getAsDouble()
BooleanSupplierboolean getAsBoolean()

IntStream.filter is an IntPredicate. forEach on that stream is an IntConsumer. OptionalInt.orElseGet / IntStream.generate take an IntSupplier.

ObjIntConsumer is the mixed cousin of BiConsumer: one object, one primitive, no Integer box on the second argument.

Map<String, Integer> qtyBySku = new HashMap<>();
ObjIntConsumer<String> addQty = (sku, qty) -> qtyBySku.merge(sku, qty, Integer::sum);

for (LineItem item : order.items()) {
    addQty.accept(item.sku(), item.quantity());
}

accept does not box qty. merge still stores Integer — the mix is the SAM shape, not a zero-allocation map.

Where the JDK already asks

You do not invent these types for a helper that maps emails. You fill a slot that already chose the primitive. Streams Advanced is where primitive streams get the full pipeline treatment; this table is the SAM that fills each slot.

APISlot
Stream.mapToIntToIntFunction
Stream.mapToLong / mapToDoubleToLongFunction / ToDoubleFunction
Collectors.summingInt / summarizingIntToIntFunction
Comparator.comparingIntToIntFunction
IntStream.mapIntUnaryOperator
IntStream.mapToLong / mapToDoubleIntToLongFunction / IntToDoubleFunction
IntStream.mapToObjIntFunction
IntStream.filterIntPredicate
IntStream.forEachIntConsumer
IntStream.reduceIntBinaryOperator
IntStream.generate / OptionalInt.orElseGetIntSupplier
AtomicInteger.updateAndGetIntUnaryOperator
AtomicInteger.accumulateAndGetIntBinaryOperator
Arrays.setAll(int[], …)IntUnaryOperator

LongStream and DoubleStream repeat the same grid with long / double. Once mapToInt has produced an IntStream, every later op is already the primitive SAM — .filter(q -> q > 0) is an IntPredicate whether you name the type or not.

int unitsSold = orders.stream()
        .filter(Order::active)
        .flatMap(o -> o.items().stream())
        .mapToInt(LineItem::quantity)
        .filter(q -> q > 0)          // IntPredicate
        .sum();

IntStream.range(0, items.size()).mapToObj(items::get) is an IntFunction<LineItem> — index in, object out.

When to skip the primitive SAM

Fill the slot IntStream already declared. Do not reach for ToIntFunction because the catalog looks complete.

Stay on the object types when:

  • The pipeline maps ids, emails, or BigDecimal totals — there is nothing to unbox.
  • You have ten orders, not a profiler pointing at Integer.valueOf in a hot loop.
  • The public seam has a domain verb. Export itemCount(Order), not ToIntFunction<Order>. DiscountPolicy still returns BigDecimal; PaymentGateway is still a named collaborator — not an IntConsumer of cents.
  • You would have to invent a primitive slot. List.forEach takes a Consumer. It does not take an ObjIntConsumer.

The hub calls this out as cargo-cult: ToIntFunction in ordinary application code is a badge, not a design. The type is for mapToInt, comparingInt, summingInt, and the rare numeric helper you wrote to match those APIs.

Note: Extracting Order.total() to cents so you can write ToIntFunction<Order> is not a win. You still did BigDecimal work, and you now have a rounding policy hiding in a SAM name. Keep money as BigDecimal. Primitive SAMs are for values that were already int / long / double — LineItem.quantity(), a count, a score.

Pitfalls

Accidental boxing back. mapToInt then .boxed() to List<Integer> pays the allocation you just avoided. So does mapToObj that only wraps the int. Stay on IntStream through sum, average, or toArray if the result is still numeric.

The slot boxes, not the method reference. LineItem::quantity is an int getter. In a Function<LineItem, Integer> slot it still boxes. In a ToIntFunction<LineItem> slot it does not. Same writing, different tax.

ToIntFunction<LineItem> qty = LineItem::quantity;           // primitive
Function<LineItem, Integer> boxed = LineItem::quantity;     // boxes

ObjIntConsumer vs BiConsumer. BiConsumer<String, Integer> compiles for (sku, qty) and boxes qty. Use ObjIntConsumer<String> when the second argument is a primitive you are trying not to wrap. Do not substitute it into a Map.forEach / BiConsumer slot — those want two objects.

Narrowing is a cast. DoubleToIntFunction and LongToIntFunction truncate. They are not Math.round. If the conversion has a policy, write the policy; do not hide it in the SAM name.

Cheat sheet

Why          skip Integer/Long/Double on a hot numeric path
How to read  Int* = primitive arg    ToInt* = primitive return
             ObjInt = object + unboxed int
             Supplier prefix = return (IntSupplier, BooleanSupplier)

Meet first   Stream.mapToInt (ToIntFunction)    IntStream.filter (IntPredicate)
Then         IntStream.map (IntUnaryOperator)   reduce (IntBinaryOperator)
             mapToObj (IntFunction)             orElseGet (IntSupplier)
Also         BooleanSupplier — Predicate already returns boolean

Object twin  Function / Predicate / Consumer / Supplier
             UnaryOperator / BinaryOperator    BiFunction / BiConsumer

Do           fill the slot IntStream already chose
Don't        ToIntFunction in ordinary checkout code
             boxed() right after mapToInt
             money as int cents to “use primitives”

Do:

  • Use mapToInt(LineItem::quantity) (or comparingInt, summingInt) when the value is already an int and the pipeline is numeric.
  • Let IntStream pick IntPredicate / IntConsumer / IntUnaryOperator for you.
  • Keep Order.total() as BigDecimal.

Don’t:

  • Reach for ToIntFunction because the grid looks complete.
  • Box back with .boxed() / Function<LineItem, Integer> on the path you just unboxed.
  • Treat ObjIntConsumer as a drop-in for BiConsumer.

Wrap-up

java.util.function is four shapes, then same-type, then two-arg, then this grid. The primitive SAMs skip the Integer tax on a hot path and fill the slots IntStream / mapToInt already declared. When boxing is not the tax, stay on Function. The hub is the map of the series; this page is the last family on it.