A dive deep on Java 8 IntStream

Introduction

The Javadoc of IntStream says,

A sequence of primitive int-valued elements supporting sequential and parallel aggregate operations. This is the int primitive specialization of Stream.

The Java 8 IntStream class enables us to operate on a sequence of ints as a Stream. It differs from a Stream<Integer>. When using a Stream, the elements in the stream cannot be int primitives. They are the boxed version of the int primitives – the Integer Object. The IntStream provides all the operations that exist on the Stream. We can thus use them on a sequence of ints (without boxing and unboxing).

IntStream – Generation

There are ways to create finite and infinite integer streams.

Finite stream

The easiest way to create an Intstream is to use the static factory method ofIt has a method that accepts an varargs of integer elements (int … values). It then returns an ordered IntStream backed up by those elements.

IntStream.of(1, 2, 3)
    .forEach(System.out::println);

Using the range method, we can create an ordered Intstream from a range of numbers. The second parameter passed to the range method is exclusive. In other words, it generates an Intstream from [start … end – 1].

IntStream.range(0, 5)
    .forEach(System.out::println);

prints

0
1
2
3
4

There is a method rangeClosed that is similar to the above method. But it considers the end parameter as inclusive.

IntStream.rangeClosed(0, 5)
    .forEach(System.out::println);

Infinite stream

IntStream.generate static method takes an IntSupplier parameter. The IntSupplier represents a supplier of int values. It is the int primitive specialization of a Supplier<Integer>. The returned Intstream is an infinite sequential unordered stream.

AtomicInteger atomicInteger = new AtomicInteger(0);
 IntStream.generate(atomicInteger::incrementAndGet) 
            .limit(5)
            .forEach(System.out::println);

Here, I have used an atomic integer to generate an infinite stream of int values. To make the example realistic (and to end), I have limited the stream to first five elements. This prints numbers 1 to 5.

IntStream.iterate static method returns an infinite sequential ordered stream based on a seed and IntUnaryOperator. It generates the next element based on the previous element. The IntUnaryOperator is an int primitive specialization of an UnaryOperator. UnaryOperator is a function where the type of the operand and the result are the same i.e., Function<T, T>.

IntStream.iterate(0, i -> i + 1)   
     .limit(5)
     .forEach(System.out::println);

Again, limiting the stream to five elements. This prints numbers 0 to 4.

IntStream – Intermediate operations

Let us now look at some intermediate operations available to manipulate the Intstream.

filter and map

With the map function, we can map an element in the stream to a new value. As with all stream operations, this does not mutate the stream. It will produce a new stream with the map operation applied to each of the elements. For instance, let us create an Intstream from 0 to 9, pick only the even numbers, double them and print them.

IntStream.range(0, 10)
    .filter(num -> num % 2 == 0)
    .map(num -> num * 2) //.mapToObj(num -> num * 2)
    .forEach(System.out::println);

Note: The output of the map must be an int. If we want to produce an object as a result of the map operation, we can use mapToObj. Use mapToLong and mapToDouble to convert the int values to Long and Double, respectively.

flatmap

When the map method maps one int element to a stream of ints and when we want to flatten the stream, we can use flatMap. In the below example, each element of the stream generates an Intstream of three elements – the num – 1, the num and the num + 1.

IntStream.rangeClosed(1, 5)
    .flatMap(num -> IntStream.rangeClosed(num - 1, num + 1))
    .forEach(System.out::println);

distinct and sorted

As the name suggests, distinct removes duplicates and sorted sorts the stream.

IntStream.of(3, 2, 2, 1, 1, 3)
    .distinct()
    .sorted()
    .forEach(System.out::println);

IntStream – Terminal operations

Here are some of the terminal operations supported by Intstream. Since, they are simple enough to understand, I’m not showing code samples for them.

MethodDescription
anyMatchReturns true if at least one element match the provided predicate
allMatchReturns true iff all the elements match the provided predicate
noneMatchReturns true iff none of the elements match the provided predicate
sumReturns the sum of elements in this stream.
countReturns the number of elements in this stream.
minReturns the minimum element of this stream. It returns an OptionalInt
since the stream could be empty.
maxReturns the maximum element of this stream. It returns an OptionalInt
since the stream could be empty
averageReturns the arithmetic mean of the elements of this stream. It returns an
OptionalDouble since the stream could be empty.

reduce

The reduce is used to perform a reduction on the stream of elements. There are two variants of the reduce function – one takes the identity value for the reduce function and one that doesn’t.
The function we pass to the reduce method is called as the accumulator function. The current result and the current element are passed to the accumulator function.The below code reduces a stream by adding 10 times of each integer.

int result = IntStream.range(0, 10)
        .reduce(0, (a, b) -> a + (b * 10));
System.out.println(result);

This outputs 450

Here’s the version of the same function that does not take an identity value.

IntStream.range(0, 10)
    .reduce((a, b) -> a + (b * 10))
    .ifPresent(System.out::println);

collect

The collect method enables us to perform a reduction by collecting the result in a mutable result container. Thus it does a mutable reduction. It takes three parameters

  • supplier: The supplier of the result container.
  • accumulator: The function called with the above container and an element. This is called for each element in the stream.
  • combiner: The function called with partial result of two values/containers. This is applicable only in parallel streams where this function is used to combine the results of the containers produced by two threads.

This code just adds the elements in the stream to an ArrayList.

List<Integer> result = IntStream.range(0, 10)
     .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
System.out.println(result);

Since, this is a sequential stream, the combiner function will not be executed at all. 
To see it in action, in addition, let us make the stream parallel and add a print statement to the combiner. 

List<Integer> result = IntStream.range(0, 10)
        .parallel()
        .collect(ArrayList::new, ArrayList::add, (list1, list2) -> {
            System.out.println("Combiner called for " + list1 + " and " + list2);
            list1.addAll(list2);
        });
System.out.println(result);

You can see that it prints out lines like (Note: The values can vary from run to run)

Combiner called for [0] and [1]
Combiner called for [8] and [9]
Combiner called for [3] and [4]
Combiner called for [7] and [8, 9]
Combiner called for [5] and [6]
Combiner called for [2] and [3, 4]
Combiner called for [0, 1] and [2, 3, 4]
Combiner called for [5, 6] and [7, 8, 9]
Combiner called for [0, 1, 2, 3, 4] and [5, 6, 7, 8, 9]

We can see that the reduction has happened in parallel and it uses the combiner to combine the results of the various reduction operations.

Conclusion

In this post, we looked at the stream implementation for the primitive int typed values – the IntStream. We dived deep on the various methods of creation. We had a look at the intermediate and terminal operations on the IntStream.

Leave a Reply