Back to Blog
Java

Java Stream count(): Counting Elements

java stream count: Learn how to use the count() terminal operation in Java Streams, including filtering, return types, parallel behavior, and common pitfalls.

javastream-apicountterminal-operationsparallel-streams
Illustration of counting elements flowing through a Java stream pipeline with a counter display

Java's Stream interface provides count() as a terminal operation that returns the number of elements in the stream. It is the simplest way to perform a java stream count without collecting elements into a collection first.

List<String> names = List.of("ada", "alan", "grace", "linus"); long total = names.stream().count(); System.out.println(total); // 4

The method returns long, not int. A stream can contain more elements than the maximum int value, especially when it is backed by a generator, a file, or a database cursor. The long return type reflects that streams are not limited to array-sized inputs.

Because count() is a terminal operation, it consumes the stream. After calling it, the stream cannot be reused. If you need the elements again, you must create a new stream from the source.

Counting Elements That Match a Condition

The most common use of java stream count is counting elements that satisfy a predicate. You combine filter() with count():

List<String> names = List.of("ada", "alan", "grace", "linus"); long longNames = names.stream() .filter(name -> name.length() > 3) .count(); System.out.println(longNames); // 3

The filter runs lazily. count() triggers the pipeline to process each element, and only elements that pass the predicate contribute to the result. This is equivalent to a loop with an incrementing counter, but it keeps the logic declarative and composes with other stream operations.

You can chain multiple filters before counting:

long result = orders.stream() .filter(order -> order.isPaid()) .filter(order -> order.getTotal() > 100) .count();

Each filter narrows the set of elements that reach the counter. The order of filters matters for performance only when one predicate is significantly cheaper or more selective than another; the stream processes elements one at a time through the whole pipeline.

Why count() Returns long

The return type is a deliberate API decision. int has a maximum value of 2,147,483,647. A stream sourced from a large file, a database result set, or an infinite generator can exceed that limit in theory. Returning long avoids silent overflow when counting large streams.

This matters in practice when you assign the result to a variable. If you write:

int total = stream.count(); // does not compile

The compiler rejects it because long cannot be implicitly narrowed to int. You must either declare the variable as long or cast explicitly:

long total = stream.count();

Casting to int is only safe when you know the stream size is small, but it discards the safety the API provides. Prefer keeping the result as long unless an external API requires an int.

How count() Behaves on Infinite and Parallel Streams

count() must traverse every element to produce a result. There is no shortcut that knows the size of an arbitrary stream in advance. For a stream backed by a List, the count is still computed by iterating, not by reading list.size(). The stream API does not special-case collection sources for count().

This has a direct consequence for infinite streams:

Stream.generate(() -> "x").count(); // never terminates

A stream created with generate(), iterate(), or Stream.concat() of unbounded sources has no finite size. Calling count() on it will run forever. If you need a count from such a source, you must bound it first with limit():

long count = Stream.generate(() -> "x") .limit(1_000_000) .count();

Parallel streams behave differently. count() is a reduction that does not require an identity value or an accumulator function, so the runtime can split the stream across threads and sum the partial counts. For large collections, parallelStream().count() can be faster than the sequential version, but the overhead of splitting and merging only pays off when the stream is large and the pipeline is not trivial. For small collections, sequential counting is usually faster.

Comparing count() With Collectors.counting()

The Collectors class offers counting() as an alternative:

long total = names.stream().collect(Collectors.counting());

Both produce the same result for a finite stream. The difference is where they fit in a pipeline. count() is a terminal operation that ends the stream. Collectors.counting() is a collector, which means it can be combined with groupingBy() or partitioningBy() to count elements within groups:

Map<String, Long> countByStatus = orders.stream() .collect(Collectors.groupingBy(Order::getStatus, Collectors.counting()));

Here Collectors.counting() is the downstream collector that counts how many orders fall into each status group. You cannot use Stream.count() in that position because it is not a collector.

For a plain total count, use count(). It is shorter, clearer, and avoids the overhead of building a collector. Use Collectors.counting() only when you need a count as part of a larger collection operation.

Common Mistakes When Counting Stream Elements

One frequent error is treating count() as a non-consuming operation. Because it is terminal, you cannot call it and then continue using the same stream:

Stream<String> stream = names.stream(); long total = stream.count(); stream.filter(...); // IllegalStateException: stream has already been operated upon or closed

The stream is consumed after the terminal operation. Recreate the stream from the source if you need to process it again.

Another mistake is assuming count() reflects the size of the source collection. If the pipeline contains distinct(), skip(), or limit(), the count reflects the transformed stream, not the original collection:

long unique = names.stream().distinct().count();

This is usually what you want, but it is worth remembering that the count is the number of elements that survive the entire pipeline.

A third issue is counting with filter() when a more specific operation exists. For example, anyMatch() and noneMatch() answer boolean questions without counting all matching elements. If you only need to know whether at least one element matches, use anyMatch():

boolean hasLongName = names.stream().anyMatch(name -> name.length() > 3);

This can short-circuit as soon as a match is found, while filter().count() > 0 must traverse the whole stream. For large streams, the difference is meaningful.

Counting in a Single Pass With Other Reductions

When you need both a count and another aggregate, you can combine reductions in a single pass instead of traversing the stream twice. Collectors.teeing() (added in Java 12) lets you apply two collectors to the same stream:

record Stats(long count, int totalLength) {} Stats stats = names.stream().collect(Collectors.teeing( Collectors.counting(), Collectors.summingInt(String::length), Stats::new ));

This traverses the stream once and produces both the count and the sum of string lengths. Without teeing(), you would call count() and mapToInt().sum() separately, which means two full traversals of the source.

Use this pattern when the stream source is expensive to traverse, such as a file-backed stream or a remote query result. For an in-memory list, the cost of a second traversal is usually negligible, and two separate, readable operations are often clearer than a teeing() combination.

The same idea applies when you need a count alongside a groupingBy() result. You can count within groups while also collecting the elements themselves, but that requires two downstream collectors and is rarely worth the complexity unless the source is genuinely expensive to read twice.

java stream count: Practical Usage and Code Examples | RYUSLOG DEV