Autoboxing is a built-in mechanism in Java for automatically converting primitive types to and from their wrapper class instances:

1
2
3
4
Integer myMethod(Integer factor) {
  int result = 5 * input; // 'factor' gets unboxed to 'int'
  return result; // 'result' gets boxed to 'Integer'
}

Even though this mechanism is powerful and ubiquitous in daily work with Java, it can have a negative performance impact if used excessively. The performance problem arises when we uselessly convert many values back and forth between primitive and its wrapper classes. This overuse can often appear when working with generics, as they do not support primitive types yet (which will change with project Valhalla).

The collection API is a classic example of this. Performance problems can quickly arise from unnecessary autoboxing while working with the values of a collection, e.g., calculating the sum of a List<Integer>. To work around this, some excellent libraries provide primitive collection alternatives.

Since Java 8, the Stream<T> API, functional interfaces, and Optional<T>s are integral parts of many libraries and code bases. However, almost all of these functions are working on object values and generics, which brings us back to the same autoboxing performance problem.

Fortunately for us, the authors of these features have built alternative primitive solutions into the Java standard library. However, these are often not used in practice, so that we will take a closer look at them in this article.

Primitive Functional Interfaces 

Every interface which has one single abstract (and therefore unimplemented) method is called a functional interface. This interface is the foundation for lambda expression, as they are the implementation of the abstract method. For example, the functional interface Function<String, Integer> can be implemented via the lambda expression input -> input.length().

When we define functional interfaces ourselves, there is no restriction on using primitive parameters and a primitive return value for the abstract method.

However, since specific functional interface patterns occur over and over again, we don’t always want to reinvent the wheel. For that, the Java standard library contains a series of commonly used functional interfaces (e.g., Supplier<T> or Function<T, R>).

To avoid always having to work with the wrapper classes of primitive data types, the standard library has primitive interface alternatives for the data types int, long and double:

With Wrapper Class Primitive Alternative
BinaryOperator<Integer> IntBinaryOperator
BinaryOperator<Long> LongBinaryOperator
BinaryOperator<Double> DoubleBinaryOperator
Consumer<Integer> IntConsumer
Consumer<Long> LongConsumer
Consumer<Double> DoubleConsumer
BiConsumer<T, Integer> ObjIntConsumer
BiConsumer<T, Long> ObjLongConsumer
BiConsumer<T, Double> ObjDoubleConsumer
Function<Integer, R> IntFunction
Function<Long, R> LongFunction
Function<Double, R> DoubleFunction
Function<Integer, Integer> IntUnaryOperator
Function<Long, Long> LongUnaryOperator
Function<Double, Double> DoubleUnaryOperator
Function<Integer, Double> IntToDoubleFunction
Function<Integer, Long> IntToLongFunction
Function<Long, Integer> LongToIntFunction
Function<Long, Double> LongToDoubleFunction
Function<Double, Integer> DoubleToIntFunction
Function<Double, Long> DoubleToLongFunction
Function<T, Integer> ToIntFunction
Function<T, Long> ToLongFunction
Function<T, Double> ToDoubleFunction
BiFunction<T, U, Integer> ToIntBiFunction<T, U>
BiFunction<T, U, Long> ToLongBiFunction<T, U>
BiFunction<T, U, Double> ToDoubleBiFunction<T, U>
Predicate<Integer> IntPredicate
Predicate<Long> LongPredicate
Predicate<Double> DoublePredicate
Supplier<Integer> IntSupplier
Supplier<Long> LongSupplier
Supplier<Double> DoubleSupplier

Primitive Stream API 

The functional interfaces from the previous chapter are often used in the stream API because their lambda implementation allows programming a simple, modular, and well-readable processing chain of values.

In practice, we can observe an increase in the autoboxing overhead in the stream API. This increase is mainly a result of using small processing steps that do not reuse intermediate values.

Let’s illustrate this problem with the following example. Instead of unboxing the value once to a primitive, it gets unboxed, processed, and boxed again in each lambda step:

1
2
3
4
5
6
7
Stream.of(1, 2, 3, 4, 5) // Values are getting boxed to 'Integer'
  // 'i' is an 'Integer' and gets unboxed to 'int'
  .filter(i -> i % 2 == 0)
  // 'i' is an 'Integer' and gets unboxed to 'int'
  // and the return value gets boxed back to 'Integer'
  .map(i -> i * 2)
  ...

To get around the unnecessary autoboxing problem, the stream API has three primitive alternatives besides the very familiar Stream interface: IntStream, LongStream, and DoubleStream.

The primitive stream interfaces provide the same functionality as the object stream interface but use the primitive functional interfaces shown in the previous chapter, which have primitive parameters and return values.

Using a primitive stream, we can convert the example shown above so that it works without any autoboxing overhead:

1
2
3
4
5
6
IntStream.of(1, 2, 3, 4, 5) // Vales are directly used as 'int's
  // 'i' is an 'int'
  .filter(i -> i % 2 == 0)
  // 'i' is an 'int' and return value is also an 'int'
  .map(i -> i * 2)
  ...

Converting Between Primitive and Object Stream 

Sometimes, it is inevitable that we start with an object-based stream but still want to take advantage of a primitive stream. For this, there are six conversion methods in the Stream interface:

1
2
3
4
5
6
7
8
IntStream mapToInt(ToIntFunction<? super T>)
IntStream flatMapToInt(Function<? super T, ? extends IntStream>)
  
LongStream mapToLong(ToLongFunction<T>)
LongStream flatMapToLong(Function<? super T, ? extends LongStream>)
  
DoubleStream mapToDouble(ToDoubleFunction<? super T>)
DoubleStream mapToDouble(Function<? super T, ? extends DoubleStream>)

Conversely, if we have a primitive stream, we can use the mapToObj() method to convert to an object stream.

Summary Statistics 

Sometimes we want to determine specific characteristics of a number set, e.g., the minimum, maximum, and sum. If we’re going to do this from an object stream, we would have to keep track of these characteristics in variables outside the stream. However, this approach breaks the nice continuous processing line of chained stream methods.

With the “summary statistics” feature, the three primitive streams offer an elegant solution for this:

1
2
3
4
5
6
IntSummaryStatistics summaryStatistics = IntStream.of(1, 2, 3, 4, 5).summaryStatistics();
summaryStatistics.getCount(); // 5
summaryStatistics.getMax(); // 5
summaryStatistics.getMin(); // 1
summaryStatistics.getAverage(); // 3.0
summaryStatistics.getSum(); // 15

Primitive Optionals 

An Optional is a container object that gets used to express a value’s (non-)presence. We should use this object primarily as the return value of methods since it has the advantage of forcing the caller to deal with the potential non-existence of a value (a potentially null value gets often overlooked because no one read the JavaDoc).

Similar to the object stream, we can quickly fall into the same unnecessary autoboxing trap here if we use the object-based Optional<T>:

1
2
3
4
5
Optional.of(2) // Value is boxed to 'Integer'
  // 'i' is an 'Integer' and gets unboxed to 'int'
  .filter(i -> i % 2 == 0)
  // 'i' is an 'Integer' and gets unboxed to 'int'
  .ifPresent(i -> ...);

Analogous to primitive streams, there are primitive alternatives to the object-based one: OptionalInt, OptionalLong, and OptionalDouble.

Likewise, these offer the same functionalities as the object-based optional only with the difference that primitive functional interfaces and primitive parameters and return values are used:

1
2
3
4
5
6
7
OptionalInt OptionalInt#of(int)

int OptionalInt#getAsLong()
int OptionalInt#orElse(int)
int OptionalInt#orElseGet(IntSupplier)
void OptionalInt#ifPresent(IntConsumer)
void OptionalInt#ifPresentOrElse(IntConsumer, Runnable)
About the Author

Marcel Kliemannel

Software Engineer, JVM Enthusiast and technical writer with a focus on architecture, backend, security, automation, DevOps, monitoring, and performance.

Related Articles