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 name | Means |
|---|---|
Int / Long / Double as a prefix | that primitive is an argument |
ToInt / ToLong / ToDouble | that primitive is the return |
ObjInt (and ObjLong / ObjDouble) | object + primitive, the int stays unboxed |
applyAsInt / getAsInt / test | same 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
| Type | SAM | Reads 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 |
IntToLongFunction | long applyAsLong(int) | int → long |
IntToDoubleFunction | double applyAsDouble(int) | int → double |
LongToIntFunction | int applyAsInt(long) | long → int |
LongToDoubleFunction | double applyAsDouble(long) | long → double |
DoubleToIntFunction | int applyAsInt(double) | double → int |
DoubleToLongFunction | long 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.
| Type | SAM | Reads as |
|---|---|---|
IntUnaryOperator | int applyAsInt(int) | int → int |
LongUnaryOperator | long applyAsLong(long) | long → long |
DoubleUnaryOperator | double applyAsDouble(double) | double → double |
IntBinaryOperator | int applyAsInt(int, int) | (int, int) → int |
LongBinaryOperator | long applyAsLong(long, long) | (long, long) → long |
DoubleBinaryOperator | double 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
| Type | SAM |
|---|---|
IntPredicate | boolean test(int) |
LongPredicate | boolean test(long) |
DoublePredicate | boolean test(double) |
IntConsumer | void accept(int) |
LongConsumer | void accept(long) |
DoubleConsumer | void accept(double) |
ObjIntConsumer<T> | void accept(T, int) |
ObjLongConsumer<T> | void accept(T, long) |
ObjDoubleConsumer<T> | void accept(T, double) |
IntSupplier | int getAsInt() |
LongSupplier | long getAsLong() |
DoubleSupplier | double getAsDouble() |
BooleanSupplier | boolean 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.
| API | Slot |
|---|---|
Stream.mapToInt | ToIntFunction |
Stream.mapToLong / mapToDouble | ToLongFunction / ToDoubleFunction |
Collectors.summingInt / summarizingInt | ToIntFunction |
Comparator.comparingInt | ToIntFunction |
IntStream.map | IntUnaryOperator |
IntStream.mapToLong / mapToDouble | IntToLongFunction / IntToDoubleFunction |
IntStream.mapToObj | IntFunction |
IntStream.filter | IntPredicate |
IntStream.forEach | IntConsumer |
IntStream.reduce | IntBinaryOperator |
IntStream.generate / OptionalInt.orElseGet | IntSupplier |
AtomicInteger.updateAndGet | IntUnaryOperator |
AtomicInteger.accumulateAndGet | IntBinaryOperator |
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
BigDecimaltotals — there is nothing to unbox. - You have ten orders, not a profiler pointing at
Integer.valueOfin a hot loop. - The public seam has a domain verb. Export
itemCount(Order), notToIntFunction<Order>.DiscountPolicystill returnsBigDecimal;PaymentGatewayis still a named collaborator — not anIntConsumerof cents. - You would have to invent a primitive slot.
List.forEachtakes aConsumer. It does not take anObjIntConsumer.
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)(orcomparingInt,summingInt) when the value is already anintand the pipeline is numeric. - Let
IntStreampickIntPredicate/IntConsumer/IntUnaryOperatorfor you. - Keep
Order.total()asBigDecimal.
Don’t:
- Reach for
ToIntFunctionbecause the grid looks complete. - Box back with
.boxed()/Function<LineItem, Integer>on the path you just unboxed. - Treat
ObjIntConsumeras a drop-in forBiConsumer.
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.