Course contents

Primitive streams: IntStream, LongStream and DoubleStream

Primitive streams in Java 21: IntStream, LongStream, DoubleStream, boxing cost, numeric summaries, ranges, string chars, conversions and overflow traps.

PremiumOften askedLesson14 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: a checkout belt with and without boxes
  2. Primitive streams under the hood: boxing, ranges and conversions
  3. Summing and statistics: sum, reduce and collectors
  4. Advanced: overflow, strings and missing primitive types
  5. How to say it in the interview

🔢 What are primitive streams?

Primitive streams are stream types that carry raw numeric values instead of boxed objects: IntStream for int, LongStream for long and DoubleStream for double. They let a pipeline add, average, range over and summarize numbers without wrapping every element in Integer, Long or Double. For example, orders.stream().mapToInt(Order::cents).sum() keeps the cents as primitive int values all the way through the sum.

For this lesson, we'll focus on the primitive side of streams: why these three types exist, how mapToInt and friends move between object and primitive pipelines, how numeric terminal operations differ from reduce and collectors, how ranges work, how strings expose characters as IntStream, and where overflow or Unicode can surprise you. Interviewers ask this because the code is tiny, but the answer reveals whether you can see the cost model behind a stream pipeline.

Below the line (out of scope)

The 30-second answer

Java has IntStream, LongStream and DoubleStream because generic Stream<T> can only hold references, so Stream<Integer> means boxing. A primitive stream keeps values as primitives and adds numeric operations like sum, average, summaryStatistics, range and rangeClosed. Use mapToInt, mapToLong or mapToDouble when an object stream becomes numeric, then boxed or mapToObj when you need objects again. average() returns OptionalDouble; max() and min() return OptionalInt, OptionalLong or OptionalDouble because the stream might be empty. Watch IntStream.sum(): it returns int and silently wraps on overflow, so use mapToLong or LongStream for totals that can exceed about 2.1 billion. String.chars() returns UTF-16 code units as an IntStream; use codePoints() when emoji or supplementary characters matter.

What you’ll learn

  • Explain why primitive streams exist and where boxing changes memory and allocation behavior
  • Choose IntStream, LongStream or DoubleStream for sums, averages, ranges and numeric pipelines
  • Convert between object streams and primitive streams with mapToInt, mapToObj, boxed and asLongStream
  • Use sum, average, summaryStatistics, max and min with the right optional return types
  • Read strings with String.chars() and codePoints() without breaking surrogate pairs
  • Avoid the IntStream.sum() overflow trap by switching to mapToLong or LongStream when totals can exceed int

Interview radar

Asked often: Why use mapToInt(...).sum() instead of stream().map(...).reduce(...)? Core follow-up: What does String.chars() really stream, and why can codePoints() be safer? Production angle: A daily revenue report stored cents in int, used IntStream.sum(), and wrapped a large total into a negative number.

01Picture it first: a checkout belt with and without boxes

Picture a checkout belt carrying 10 million item prices. A primitive stream is the belt where every price is a bare 4-byte int. A Stream<Integer> is the belt where each price is first put into a small cardboard box, then the calculator opens each box before adding it.

The point is not that every boxed stream is bad. The point is that a numeric pipeline should not allocate wrappers when the API already gives you a raw-number path.

Fig. 1

A boxed Stream Integer pipeline allocates wrapper objects before summing, while an IntStream keeps raw int values through mapToInt and sum

A boxed Stream Integer pipeline allocates wrapper objects before summing, while an IntStream keeps raw int values through mapToInt and sum

Notice the three lanes. mapToInt crosses from object land to primitive land. Numeric terminals such as sum and summaryStatistics live on the primitive side. boxed and mapToObj cross back when a later API needs objects.

02Primitive streams under the hood: boxing, ranges and conversions

Try it yourself first

Before reading on, pick the pipeline you would use for summing 1 million order amounts in cents. Which one allocates wrapper objects if the JIT cannot remove them?

import java.util.List;

record Order(String id, int cents) { }

public class PrimitiveSumDemo {
    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order("o-1", 1_200),
                new Order("o-2", 800),
                new Order("o-3", 500)
        );

        int primitiveTotal = orders.stream()
                .mapToInt(Order::cents)
                .sum();

        int boxedTotal = orders.stream()
                .map(Order::cents)
                .reduce(0, Integer::sum);

        System.out.println(primitiveTotal); // 2500
        System.out.println(boxedTotal);     // 2500
    }
}

Both versions print the same result. The first one communicates the numeric intent better and gives the runtime a primitive pipeline. The second one creates a Stream<Integer> shape, which means values are references unless optimization removes the boxes.

Notice three things:

  • Stream<T> is generic, and Java generics do not accept primitive type arguments. There is no Stream<int>.
  • mapToInt takes a ToIntFunction<T>, so the extractor returns a primitive int through applyAsInt.
  • IntStream.sum() is a terminal operation. Once you call it, the pipeline is consumed, just like any other stream terminal.
Performance angle

On a typical 64-bit HotSpot JVM with compressed ordinary object pointers, an Integer wrapper is commonly 16 bytes: a 12-byte object header, a 4-byte int value, and alignment already satisfied. A raw int is 4 bytes. If a pipeline boxes 10 million non-cached integers and those boxes escape optimization, that is roughly 160 MB of wrapper objects before counting references, compared with 40 MB for the raw values. Escape analysis can remove some boxes, but I do not design hot numeric code around the optimizer saving a shape I did not need.

The three primitive stream types

Java gives exactly three primitive stream families:

Type Element Common sources Common terminals
IntStream int mapToInt, IntStream.range, String.chars sum, average, summaryStatistics, max, min
LongStream long mapToLong, LongStream.range, asLongStream sum, average, summaryStatistics, max, min
DoubleStream double mapToDouble, Random.doubles sum, average, summaryStatistics, max, min

There is no CharStream, ByteStream, ShortStream or FloatStream. The usual choice for char, byte and short is IntStream, because Java numeric promotion already treats them as int in many expressions. For float, use DoubleStream when the stream API is a good fit, or keep a loop if preserving float precision and memory layout is the point.

Ranges: range excludes the end, rangeClosed includes it

IntStream.range(start, endExclusive) behaves like a classic for (int i = start; i < end; i++). rangeClosed(start, endInclusive) includes the right side.

import java.util.stream.IntStream;

public class RangeDemo {
    public static void main(String[] args) {
        System.out.println(IntStream.range(1, 4).boxed().toList());       // [1, 2, 3]
        System.out.println(IntStream.rangeClosed(1, 4).boxed().toList()); // [1, 2, 3, 4]

        long count = LongStreamLike.countDays(1, 31);
        System.out.println(count); // 31
    }
}

class LongStreamLike {
    static long countDays(long firstDay, long lastDay) {
        return java.util.stream.LongStream.rangeClosed(firstDay, lastDay).count();
    }
}

I use range for zero-based indexes and half-open intervals because it matches arrays and List indexes. I use rangeClosed for domain ranges where both endpoints are spoken out loud: days 1 through 31, years 2020 through 2026, invoice numbers 1000 through 1010.

Many candidates lose a boundary element here. Say the endpoint rule in the variable name if the code is easy to misread: endExclusive, lastDayInclusive, fromInclusive.

Moving between object and primitive streams

Primitive streams are not subclasses of Stream<Integer>. They are separate interfaces with conversion methods at the edges.

import java.util.List;

record Product(String sku, int grams) { }

public class ConversionDemo {
    public static void main(String[] args) {
        List<Product> products = List.of(
                new Product("p-1", 200),
                new Product("p-2", 750),
                new Product("p-3", 1_200)
        );

        List<String> heavySkus = products.stream()
                .mapToInt(Product::grams)       // Product -> int
                .filter(grams -> grams >= 500)  // still IntStream
                .asLongStream()                 // int -> long widening
                .mapToObj(grams -> grams + "g")
                .toList();

        List<Integer> boxedWeights = products.stream()
                .mapToInt(Product::grams)
                .boxed()
                .toList();

        System.out.println(heavySkus);     // [750g, 1200g]
        System.out.println(boxedWeights);  // [200, 750, 1200]
    }
}

Premium lesson

Keep reading with the Java Pass

The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.

  • Summing and statistics: sum, reduce and collectors
  • Advanced: overflow, strings and missing primitive types
  • How to say it in the interview

From ₹199 · one-time payment · No auto-renewal

Practise this topicStream basics: interview questions & scenarios
Further reading (8)