Primitive Type Streams in Java: IntStream, LongStream and More

Primitive type streams in Java with IntStream, LongStream and DoubleStream, covering creation, sum and average, overflow, summary statistics and boxing.

Conversion methods between Stream of objects, IntStream, LongStream and DoubleStream

Primitive type streams in Java are the three stream types IntStream, LongStream and DoubleStream, which hold int, long and double values instead of Integer, Long and Double objects. They avoid boxing every number and add math methods such as sum(), average() and summaryStatistics() that a Stream<Integer> does not have.

We use primitive streams to total prices, average ratings, count numbers in a range, generate index sequences and read statistics from arrays or record fields. Whenever a pipeline ends in a number, a primitive stream is the shorter and cheaper path in most cases.

The following example gets the common figures from a week of step counts in a fitness app.

int[] steps = {4200, 8100, 6500, 12000, 9300};

int total = Arrays.stream(steps).sum();                         // 40100
double avg = Arrays.stream(steps).average().orElse(0);          // 8020.0
int best = Arrays.stream(steps).max().orElse(0);                // 12000
long goalDays = Arrays.stream(steps).filter(s -> s >= 8000).count();   // 3
List<Integer> days = IntStream.rangeClosed(1, 5).boxed().toList();     // [1, 2, 3, 4, 5]
IntSummaryStatistics stats = Arrays.stream(steps).summaryStatistics();
String summary = stats.toString();   // "IntSummaryStatistics{count=5, sum=40100, min=4200, average=8020.000000, max=12000}"

Notice that average() and max() return optional values, because an empty array has no average, while sum() returns 0. We look at why the three types exist, every way to create them, the math methods and their overflow trap, and how to convert between primitive and object streams. The Java Stream tutorials page lists the other stream topics.

1. Primitives and Wrapper Classes

Java has eight primitive types, namely byte, short, int, long, float, double, char and boolean. Each has a wrapper class (Byte, Short, Integer, Long, Float, Double, Character and Boolean) so the value can be stored where an object is required, such as in a List or a generic type.

Converting a primitive to its wrapper is called autoboxing, and the reverse is unboxing. The compiler inserts both conversions for us, but each boxed value is a separate object on the heap that the garbage collector must clean up later.

List<Integer> ratings = List.of(4, 5, 3);
int sumBoxed = ratings.stream().reduce(0, Integer::sum);          // 12, unboxes and boxes each step
int sumPrimitive = ratings.stream().mapToInt(Integer::intValue).sum();   // 12, unboxes once per element

2. Support for Primitive Streams

Generics in Java work only with reference types, so a Stream<T> can never hold an int. The Stream API adds three specialized interfaces for the types that numeric code needs most, and all three support sequential and parallel pipelines.

Conversion methods between Stream of objects, IntStream, LongStream and DoubleStream
Use mapToInt() and its siblings to enter a primitive stream, boxed() or mapToObj() to leave it, and asLongStream() or asDoubleStream() to widen.
  • The IntStream interface holds int values and also covers char, short and byte data after widening.
  • The LongStream interface holds long values, for large counts, IDs and epoch milliseconds.
  • The DoubleStream interface holds double values and also covers float data after widening.

There is no CharStream, BooleanStream or FloatStream. The JDK designers chose to keep the API small, because each extra type would need its own set of functional interfaces, optionals and collectors. Character data comes as an IntStream from String.chars(), and boolean results come from counting or matching.

3. Creating Streams of Primitives

Primitive streams come from fixed values, number ranges, arrays, generators, text and object streams. The factory methods mirror the ones on Stream, plus range() and rangeClosed(), which exist only on IntStream and LongStream.

3.1. Creating Stream of Specified Values

The static of() method takes the values as varargs. An empty stream comes from empty().

int[] ints = IntStream.of(1, 2, 3).toArray();                  // [1, 2, 3]
long[] longs = LongStream.of(10L, 20L).toArray();               // [10, 20]
double[] doubles = DoubleStream.of(1.5, 2.5).toArray();         // [1.5, 2.5]
long none = IntStream.empty().count();                          // 0

3.2. range() and rangeClosed() Factory Methods

The method range(start, end) returns the numbers from start up to but not including end, in steps of 1. The method rangeClosed(start, end) includes end. Both replace the classic counting for loop, and an empty range, such as range(5, 5), gives an empty stream instead of an error.

int[] open = IntStream.range(1, 5).toArray();                  // [1, 2, 3, 4]
int[] closed = IntStream.rangeClosed(1, 5).toArray();           // [1, 2, 3, 4, 5]
long empty = IntStream.range(5, 5).count();                     // 0
long big = LongStream.rangeClosed(1, 3_000_000_000L).count();   // 3000000000

For steps other than 1, we use iterate() with a condition (Java 9), which reads like a for loop header. The generate() method repeats a supplier and needs limit() to stop.

int[] everyThird = IntStream.iterate(0, i -> i < 10, i -> i + 3).toArray();   // [0, 3, 6, 9]
int[] countdown = IntStream.iterate(3, i -> i > 0, i -> i - 1).toArray();     // [3, 2, 1]
int[] zeros = IntStream.generate(() -> 0).limit(3).toArray();                 // [0, 0, 0]

3.3. Arrays.stream()

For an int[], long[] or double[], the static method Arrays.stream() returns the matching primitive stream. Arrays have no stream() method of their own, so array.stream() does not compile. An overload takes a start and end index to stream part of the array.

int[] heartRates = {72, 88, 95, 110};
IntStream rates = Arrays.stream(heartRates);
int peak = rates.max().orElse(0);                               // 110
double avgMiddle = Arrays.stream(heartRates, 1, 3).average().orElse(0);   // 91.5
long[] ids = {101L, 102L};
long idSum = Arrays.stream(ids).sum();                          // 203
double[] kms = {2.5, 3.0};
double distance = Arrays.stream(kms).sum();                     // 5.5

Watch out for Stream.of(intArray), which returns a Stream<int[]> with one element, the array itself. Only Arrays.stream() and IntStream.of() unpack the values.

3.4. Stream mapToInt(), mapToLong() and mapToDouble()

The usual way into a primitive stream is from a stream of objects. The methods mapToInt(), mapToLong() and mapToDouble() take a function that returns the primitive value, often a getter of a record.

record Walk(String day, int steps, double km) {}
List<Walk> week = List.of(
        new Walk("Mon", 4200, 3.1),
        new Walk("Tue", 8100, 6.0),
        new Walk("Wed", 6500, 4.8));

int totalSteps = week.stream().mapToInt(Walk::steps).sum();               // 18800
double totalKm = week.stream().mapToDouble(Walk::km).sum();               // 13.899999999999999
long asLong = week.stream().mapToLong(Walk::steps).sum();                 // 18800
List<Integer> numbers = List.of(1, 2, 3);
int fromBoxed = numbers.stream().mapToInt(Integer::intValue).sum();       // 6

The kilometer total is 13.899999999999999 instead of 13.9, because double stores most decimal fractions as the nearest binary value. For money, we keep amounts in cents as long or use BigDecimal.

3.5. From Strings and Random Numbers

A String gives an IntStream of its char values through chars(), and of its Unicode code points through codePoints(). The code points version handles emoji and other characters outside the Basic Multilingual Plane correctly. A Random object returns ints(), longs() and doubles() streams within a range.

long digits = "Room 204B".chars().filter(Character::isDigit).count();   // 3
String upper = "run".chars()
        .map(Character::toUpperCase)
        .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
        .toString();                                            // "RUN"
int[] dice = new Random(7).ints(3, 1, 7).toArray();             // [5, 3, 4]

A seeded Random repeats the same sequence on every run, which keeps tests stable. The article on random number streams covers the other overloads.

4. Finding Sum, Average, Max and Min

Each primitive stream has terminal methods for common statistics. Their return types differ, and the difference matters as soon as the stream may be empty.

4.1. Built-in Methods

The table lists the result of each method on IntStream.of(10, 18, 12) and on an empty IntStream. The LongStream and DoubleStream versions return long, OptionalLong, double and OptionalDouble in the same places.

MethodReturn typeOn 10, 18, 12On an empty stream
sum()int400
count()long30
average()OptionalDoubleOptionalDouble[13.333333333333334]OptionalDouble.empty
max()OptionalIntOptionalInt[18]OptionalInt.empty
min()OptionalIntOptionalInt[10]OptionalInt.empty
int max = IntStream.of(10, 18, 12).max().orElse(0);              // 18
double average = IntStream.of(1, 2, 3, 4, 5).average().orElse(0);   // 3.0
int sum = IntStream.range(1, 10).sum();                           // 45
OptionalInt noMax = IntStream.empty().max();                      // OptionalInt.empty
int unsafe = IntStream.empty().max().getAsInt();                  // NoSuchElementException: No value present

The sum() of an IntStream returns an int and overflows without any error, so we sum large values with asLongStream().sum() or read the long sum from summaryStatistics(). A year of step counts for many users passes Integer.MAX_VALUE faster than we expect.

int overflow = IntStream.of(Integer.MAX_VALUE, 1).sum();                 // -2147483648
long correct = IntStream.of(Integer.MAX_VALUE, 1).asLongStream().sum();   // 2147483648
long viaStats = IntStream.of(Integer.MAX_VALUE, 1).summaryStatistics().getSum();   // 2147483648

4.2. Summary Statistics

When we need several figures, summaryStatistics() computes the count, sum, minimum, maximum and average in one pass. It returns an IntSummaryStatistics, LongSummaryStatistics or DoubleSummaryStatistics object, whose getSum() is a long for the int version.

IntSummaryStatistics stats = IntStream.of(10, 18, 12, 70, 5).summaryStatistics();
int min = stats.getMin();                                        // 5
int max = stats.getMax();                                        // 70
double avg = stats.getAverage();                                 // 23.0
long sum = stats.getSum();                                       // 115
long count = stats.getCount();                                   // 5

On an empty stream, the statistics object returns Integer.MAX_VALUE for getMin(), Integer.MIN_VALUE for getMax() and 0.0 for getAverage(), so we check getCount() before showing them. On an object stream, Collectors.summarizingInt() returns the same object without a separate mapToInt() step.

5. Primitive Stream to Object Stream

Collectors, toList() and most libraries work with objects, so we leave a primitive stream when the result must be a collection. The boxed() method wraps each value in its wrapper class, and mapToObj() converts each value to any object type in one step.

Stream<Integer> boxedInts = IntStream.of(1, 2, 3).boxed();
List<Integer> list = IntStream.of(1, 2, 3).boxed().toList();      // [1, 2, 3]
List<Long> longList = LongStream.of(5L, 6L).boxed().toList();     // [5, 6]
List<String> labels = IntStream.rangeClosed(1, 3).mapToObj(i -> "Day " + i).toList();   // [Day 1, Day 2, Day 3]
int[] array = IntStream.of(3, 1, 2).sorted().toArray();           // [1, 2, 3]

When the target is an array, toArray() on the primitive stream returns an int[] without any boxing. The article collecting a stream of primitives into a collection or array shows the three-argument collect() form as well.

5.1. Converting Between Primitive Streams

Widening works without a mapping function. The method asLongStream() turns an IntStream into a LongStream, and asDoubleStream() turns either one into a DoubleStream. Narrowing needs an explicit mapToInt() or mapToLong() with a cast, which can lose data.

double half = IntStream.of(1, 2).asDoubleStream().map(d -> d / 2).sum();   // 1.5
long[] widened = IntStream.of(7, 8).asLongStream().toArray();              // [7, 8]
int[] truncated = DoubleStream.of(2.9, 3.1).mapToInt(d -> (int) d).toArray();   // [2, 3]
int[] rounded = DoubleStream.of(2.9, 3.1).mapToLong(Math::round).mapToInt(Math::toIntExact).toArray();   // [3, 3]

6. Weekly Activity Report From Step Data

A fitness app sends each user a weekly report with the total steps, the daily average, the best day and how many days reached the 8,000-step goal. The raw data is a list of Walk records from section 3, and each figure is one primitive stream.

List<Walk> week = List.of(
        new Walk("Mon", 4200, 3.1), new Walk("Tue", 8100, 6.0),
        new Walk("Wed", 6500, 4.8), new Walk("Thu", 12000, 8.9),
        new Walk("Fri", 9300, 7.0));

IntSummaryStatistics steps = week.stream().collect(Collectors.summarizingInt(Walk::steps));
long totalSteps = steps.getSum();                                // 40100
double dailyAverage = steps.getAverage();                        // 8020.0
int goalDays = (int) week.stream().mapToInt(Walk::steps).filter(s -> s >= 8000).count();   // 3
String bestDay = week.stream()
        .max(Comparator.comparingInt(Walk::steps))
        .map(Walk::day)
        .orElse("none");                                         // "Thu"
double kmRounded = Math.round(week.stream().mapToDouble(Walk::km).sum() * 10) / 10.0;   // 29.8
List<String> chart = IntStream.range(0, week.size())
        .mapToObj(i -> (i + 1) + ". " + week.get(i).day() + " " + week.get(i).steps())
        .toList();                                               // [1. Mon 4200, 2. Tue 8100, 3. Wed 6500, 4. Thu 12000, 5. Fri 9300]

The long total from the statistics object stays correct even for a long history, and IntStream.range() gives each row a position number without a counter variable. For the best day, we stay on the object stream, because max() on an IntStream would return the step count but lose the day name. The post on iterating a stream with indices has more indexing patterns.

7. Primitive Type Streams in Java FAQs

Most follow-up questions about primitive streams are about the missing types, conversions and speed.

7.1. Why Is There No CharStream, FloatStream or BooleanStream?

The JDK limits the specializations to int, long and double to keep the number of interfaces manageable. A char or short widens to int, and a float widens to double, so those values fit in the existing streams. For booleans, we count or match instead.

7.2. How Do We Convert an IntStream to a List?

We call boxed().toList(), which returns an unmodifiable List<Integer>. For a modifiable list, we use boxed().collect(Collectors.toCollection(ArrayList::new)).

7.3. Is IntStream Faster Than Stream<Integer>?

Yes for number crunching, because IntStream works on raw int values, while Stream<Integer> works on Integer objects and creates a new one for every value outside the cached range of -128 to 127. The gap grows with the size of the data, so for large sums and averages we switch with mapToInt() as early as possible.

7.4. What Is the Difference Between range() and rangeClosed()?

The method range(1, 5) excludes the end value and gives 1 to 4, while rangeClosed(1, 5) includes it and gives 1 to 5. We use range(0, list.size()) for indexes, because the last valid index is size() – 1.

7.5. How Do We Sum a List of Integers?

We call list.stream().mapToInt(Integer::intValue).sum(). If the list may contain null, the unboxing throws NullPointerException, so we add filter(Objects::nonNull) before mapToInt().

8. Conclusion

The three primitive streams hold numbers without boxing them, and they add sum(), average(), max(), min() and summaryStatistics(). We create them with of(), range(), Arrays.stream(), chars() or mapToInt() on an object stream, and leave them with boxed(), mapToObj() or toArray().

Two details cause most bugs. An int sum overflows without warning, so large totals go through a LongStream or the statistics object, and average(), max() and min() return optionals that need a fallback for empty data.

9. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.