CodeGym /Courses /JAVA 25 SELF /Primitive streams and the cost of boxing

Primitive streams and the cost of boxing

JAVA 25 SELF
Level 33 , Lesson 0
Available

1. The problem: why regular streams are not always efficient

When you work with collections of numbers in Java and use regular streams (Stream<Integer>, Stream<Double>), under the hood there is “boxing” (boxing) and “unboxing” of primitive values into wrapper objects (Integer, Double, etc.). This is convenient but not always efficient:

  • Boxing — converting a primitive (int) into an object (Integer).
  • Unboxing — the reverse operation: from an object to a primitive.

Problem:
Boxing/unboxing are extra operations and consume memory. In hot (frequently invoked) parts of a program, this can lead to noticeable performance degradation, especially if you process large arrays of numbers.

Example:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);

Here, each number is an Integer object, not a primitive int.

2. Primitive streams: IntStream, LongStream, DoubleStream

To avoid unnecessary boxing/unboxing, Java has primitive streams:

  • IntStream — for int
  • LongStream — for long
  • DoubleStream — for double

They operate only on primitives and do not create extra wrapper objects.

How to create a primitive stream?

From an array:

int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);

Using range/rangeClosed:

IntStream.range(0, 10)          // 0..9
IntStream.rangeClosed(1, 5)     // 1..5 inclusive

Generating random numbers:

new Random().ints(5, 0, 100)    // 5 random ints from 0 to 99

Example: summing array elements

int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15

3. Converting between Stream<T> and primitive streams

Sometimes you have a regular stream, and other times a primitive one. Special methods are used for conversion:

  • mapToInt, mapToLong, mapToDouble — convert a regular stream into a primitive stream.
  • boxed() — turns a primitive stream back into a stream of objects.

Example:

List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3

Back:

IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();

Warning:
boxed() is the reverse operation; it creates wrapper objects (Integer, Double, etc.) again, and if you want maximum performance — avoid it in hot paths.

4. Summation and summaries: sum, average, min, max, summaryStatistics

Primitive streams provide convenient aggregation methods:

  • sum() — sum of all elements
  • average() — average (returns OptionalDouble)
  • min(), max() — minimum and maximum (OptionalInt, OptionalLong, OptionalDouble)
  • summaryStatistics() — returns an object with full statistics (sum, average, minimum, maximum, count)

Example:

int[] arr = {1, 2, 3, 4, 5};
IntSummaryStatistics stats = Arrays.stream(arr).summaryStatistics();
System.out.println(stats.getSum());      // 15
System.out.println(stats.getAverage());  // 3.0
System.out.println(stats.getMin());      // 1
System.out.println(stats.getMax());      // 5
System.out.println(stats.getCount());    // 5

Comparison with Collectors:
For regular streams you can use Collectors.summarizingInt, but that’s often less efficient than primitive-stream methods:

List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));

5. Avoiding autoboxing: where it matters and how to measure

Where is it critical?

  • In loops and streams that process large volumes of numbers (for example, array processing, statistics, math, data parsing).
  • In “hot” spots — code that is called often and impacts performance.

Why does it matter?

  • Every time boxing happens, a new wrapper object is created (Integer, Double, etc.).
  • This increases load on the garbage collector (GC).
  • In microbenchmarks, the difference can be multiple times!

How to measure?
For accurate comparison, use the JMH (Java Microbenchmark Harness) framework. It allows you to fairly compare the performance of code with and without boxing.

Example (pseudocode):

@Benchmark
public int sumIntStream() {
    return IntStream.range(0, 1_000_000).sum();
}

@Benchmark
public int sumStreamInteger() {
    return Stream.iterate(0, n -> n + 1).limit(1_000_000).reduce(0, Integer::sum);
}

The second variant will run slower due to constantly creating Integer objects.

Conclusion:
If performance matters — use primitive streams!

6. When primitive streams really help, and when not to overcomplicate

Use primitive streams if:

  • You work with large arrays/collections of numbers.
  • You need to quickly compute sum, average, minimum, maximum.
  • You write code where every millisecond counts (for example, real-time data processing).

It’s fine not to bother if:

  • The collection is small (dozens or just a handful of elements).
  • The code becomes too complex due to conversions between types.
  • Performance is not critical (for example, processing user input).

Example:

List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // It's fine, but a regular reduce would also do

Tip: don’t turn your code into a “jungle” for the sake of microscopic optimization. Use primitive streams where they’re truly justified.

7. OptionalInt, OptionalDouble: safe extraction of results

The min(), max(), average() methods on primitive streams return not just a number, but a wrapper — OptionalInt, OptionalDouble, etc. This is necessary to safely handle empty streams (for example, if an array is empty).

Example:

int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
    System.out.println("Minimum: " + min.getAsInt());
} else {
    System.out.println("Array is empty!");
}

Comparison with regular Optional:

  • OptionalInt — for int
  • OptionalDouble — for double
  • OptionalLong — for long

Why not just return 0?
Because 0 can be a valid value, and an empty stream is a different situation.

Example with average:

double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // if empty - returns NaN

8. Practice: examples of using primitive streams

Example 1: Sum of squares from 1 to 1000

int sum = IntStream.rangeClosed(1, 1000)
                   .map(x -> x * x)
                   .sum();
System.out.println(sum);

Example 2: Filtering and counting even numbers

int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
                   .filter(x -> x % 2 == 0)
                   .count();
System.out.println("Even numbers: " + count);

Example 3: Comparison with a regular Stream<Integer>

List<Integer> list = IntStream.range(0, 1_000_000)
                              .boxed()
                              .collect(Collectors.toList());

long t1 = System.currentTimeMillis();
int sum1 = list.stream().mapToInt(x -> x).sum();
long t2 = System.currentTimeMillis();
System.out.println("Stream<Integer>: " + (t2 - t1) + " ms");

t1 = System.currentTimeMillis();
int sum2 = IntStream.range(0, 1_000_000).sum();
t2 = System.currentTimeMillis();
System.out.println("IntStream: " + (t2 - t1) + " ms");

The difference will be noticeable on large data sizes.

9. Common mistakes when working with primitive streams

Error #1: Forgetting about boxing during type conversions.
If you use boxed(), remember that it creates wrapper objects again. Don’t use it unless necessary.

Error #2: Using primitive streams for objects.
IntStream works only with int, not with objects. If you need to work with objects, use a regular Stream<T>.

Error #3: Ignoring OptionalInt/OptionalDouble.
If you call min(), max(), average() — always check that the result is present (isPresent()), otherwise you’ll get an exception.

Error #4: Overly complex conversions between Stream<T> and IntStream.
If the code becomes unreadable due to constant mapToInt()boxed()mapToDouble(), it may be worth simplifying the logic.

Error #5: Expecting a “magical” speedup on small collections.
For small lists, the difference between Stream and IntStream is minimal. Don’t complicate the code for microscopic savings.

1
Task
JAVA 25 SELF, level 33, lesson 0
Locked
Daily Store Revenue Calculation 💰
Daily Store Revenue Calculation 💰
1
Task
JAVA 25 SELF, level 33, lesson 0
Locked
Analysis of spell name lengths for the Magic Academy ✨
Analysis of spell name lengths for the Magic Academy ✨
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION