Primitive functional interfaces in Java
IntPredicate, ToIntFunction… avoid boxing.
The boxing tax
Generics only work with objects, so Function<Integer, Integer> turns every int into an **Integer object** (boxing) and back again (unboxing). That's extra allocation and extra work on every call.
Function<Integer, Integer> sq = x -> x * x;
int r = sq.apply(7);
// 7 → Integer → unbox → 49 → Integer → 49Primitive specializations
java.util.function has int, long and double versions that skip boxing. Read the name: an **Int prefix means the input is an int; ToInt means the output is an int**. Method names say what they return: applyAsInt, test.
IntPredicate // int → boolean
IntFunction<R> // int → R
IntUnaryOperator // int → int
IntBinaryOperator // (int, int) → int
ToIntFunction<T> // T → int (applyAsInt)
// + Long…, Double…, IntSupplier, IntConsumerYour turn
What does this print?
IntUnaryOperator sq = n -> n * n;
ToIntFunction<String> len = s -> s.length();
IntPredicate odd = n -> n % 2 == 1;
int n = len.applyAsInt("three");
System.out.println(odd.test(n));
System.out.println(sq.applyAsInt(4));true 16false 165 16
Show the answer
"three" has length 5, which is odd → true. sq.applyAsInt(4) → 16. No boxing anywhere.
Decode the name
What does a ToDoubleFunction<String> take, what does it return, and what's its method called?
Think about it, then reveal the answer
It takes a String (the T) and returns a primitive double ("ToDouble"), via **applyAsDouble**. Example: s -> s.length() * 1.5.
Boxes behave like objects
These results are Integer objects. What does this print?
Function<Integer, Integer> id = x -> x;
IO.println(id.apply(100) == id.apply(100));
IO.println(id.apply(500) == id.apply(500));true truetrue falsefalse false
Show the answer
== compares references. Boxing uses a cache for -128 to 127, so both 100s are the same object → true. Each 500 is a new object → false. Primitive interfaces avoid this whole trap.
Hot loops
Function<Integer, Integer> inc = x -> x + 1;
int v = 0;
for (int i = 0; i < 50_000_000; i++)
v = inc.apply(v);Values above 127 need a fresh Integer on each call (unless the JIT manages to optimize it away): garbage for the collector.
IntUnaryOperator inc = x -> x + 1;
int v = 0;
for (int i = 0; i < 50_000_000; i++)
v = inc.applyAsInt(v);Plain ints from start to finish.
Where it pays off
Game loops, financial calculations and data processing use IntStream, mapToInt(ToIntFunction) and friends to stay primitive. In everyday business code the difference is small, but in a hot path, avoiding boxing can noticeably cut garbage-collection pressure.
Key takeaways
- IntPredicate, IntFunction<R>, IntUnaryOperator, IntBinaryOperator
- ToIntFunction<T>: T → int via applyAsInt
- Also Long… and Double… versions, plus IntSupplier, IntConsumer
- Avoiding boxing saves allocations in hot loops
The Integer cache's upper limit isn't fixed at 127: the JVM option -XX:AutoBoxCacheMax can raise it. The lower bound of -128 can't be changed.
Practice questions
What does this print?
IntPredicate even = n -> n % 2 == 0;
ToIntFunction<String> len = s -> s.length();
IntBinaryOperator max = (a, b) -> Math.max(a, b);
System.out.println(even.test(len.applyAsInt("four")));
System.out.println(max.applyAsInt(3, 9));- true 9
- false 9
- 4 9
- true 3
Check your answer
true 9. "four" has length 4, which is even, so test returns true. max.applyAsInt(3, 9) returns 9.
Summing 50 million numbers through a Function<Integer, Integer> is much slower than through an IntUnaryOperator. Why?
- Function is synchronized
- Each call boxes ints into Integer objects and unboxes them, creating garbage and extra work
- IntUnaryOperator runs on the GPU
- Function can't be JIT-compiled
Check your answer
Each call boxes ints into Integer objects and unboxes them, creating garbage and extra work. Boxing allocates objects (outside the small Integer cache) and adds indirection. Primitive specializations keep values as plain ints.