λ Lambdas & Functional Java · Intermediate

Primitive functional interfaces in Java

IntPredicate, ToIntFunction… avoid boxing.

🧩 The mysteryTwo loops add up 50 million numbers. Same logic. One is several times slower and creates mountains of garbage. The culprit hides in a type parameter.

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 → 49

Primitive 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, IntConsumer
🔮 Predict it

Your 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));
  1. true 16
  2. false 16
  3. 5 16
Show the answer

"three" has length 5, which is odd → true. sq.applyAsInt(4) → 16. No boxing anywhere.

🤔 Think first

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.

🔮 Predict it

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));
  1. true true
  2. true false
  3. false 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

✗ Boxes every call
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.

✓ Stays primitive
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.

💼 In the real world

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

  1. IntPredicate, IntFunction<R>, IntUnaryOperator, IntBinaryOperator
  2. ToIntFunction<T>: T → int via applyAsInt
  3. Also Long… and Double… versions, plus IntSupplier, IntConsumer
  4. Avoiding boxing saves allocations in hot loops
🤯 Did you know?

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));
  1. true 9
  2. false 9
  3. 4 9
  4. 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?

  1. Function is synchronized
  2. Each call boxes ints into Integer objects and unboxes them, creating garbage and extra work
  3. IntUnaryOperator runs on the GPU
  4. 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.

Next: s -> s.length() can be written as String::length. Four kinds of method reference, and one surprise about when they're evaluated.