Java Collectors Counting: How to Count Stream Items
Learn how to use Collectors.counting() in Java streams to count elements, group counts by key, and understand when to use Stream.count() instead.
When you need to count elements in a Java stream, Stream.count() is often the first method that comes to mind. But in many stream pipelines, especially those that build a frequency map with Collectors.groupingBy(), you need a Collector that produces a count. That is where Collectors.counting() fits. This article explains what Collectors.counting() returns, how to use it with grouping and partitioning, and when a plain count() call is the better choice.
What Collectors.counting() Returns
Collectors.counting() returns a Collector<T, ?, Long> that counts the number of input elements. The result is a Long object, not a primitive long. This matters when you store the result in a collection such as a Map, because generic types cannot use primitives.
long total = Stream.of("a", "b", "c").count(); Long totalAsLong = Stream.of("a", "b", "c").collect(Collectors.counting());
Both expressions produce the same numeric value, but the second returns Long. In most cases the difference is irrelevant because autoboxing handles the conversion, but when you are building a Map<Integer, Long>, the collector version is the one you need.
The collector is a reducing collector. It maps every element to 1L and then sums those values. For an empty stream, it returns 0L.
Counting Within groupingBy
The most common use of Collectors.counting() is as a downstream collector for groupingBy. When you group a stream by a classifier function, you often want to know how many elements fall into each group.
List<String> words = Arrays.asList("cat", "dog", "elephant", "ant"); Map<Integer, Long> countByLength = words.stream() .collect(Collectors.groupingBy(String::length, Collectors.counting())); System.out.println(countByLength); // {3=3, 8=1}
The classifier String::length produces the group key. The downstream collector counting() counts the elements in each group. Without the downstream collector, groupingBy would produce a List<String> for each key. Adding counting() changes the value type to Long.
This pattern is common when you need a frequency distribution: counts of words by length, orders by status, or events by category.
Using counting() with partitioningBy
partitioningBy is a specialized version of groupingBy that always produces two keys: true and false. It also accepts a downstream collector.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); Map<Boolean, Long> evenOddCount = numbers.stream() .collect(Collectors.partitioningBy(n -> n % 2 == 0, Collectors.counting())); System.out.println(evenOddCount); // {false=3, true=3}
Here counting() counts how many numbers satisfy the predicate and how many do not. This is more concise than collecting two separate streams and avoids traversing the input twice.
Collectors.counting() vs Stream.count()
When you only need the total number of elements in a stream, Stream.count() is the simpler choice.
long total = items.stream().count();
There is no reason to write items.stream().collect(Collectors.counting()) for a simple count. The collector version becomes useful when it is part of a larger collection operation.
| Approach | Return type | Typical use |
|---|---|---|
stream.count() | long | Total count of elements in a single stream |
collect(Collectors.counting()) | Long | Producing a count when a Collector is required |
groupingBy(key, counting()) | Map<K, Long> | Count elements per group |
partitioningBy(pred, counting()) | Map<Boolean, Long> | Count elements by a boolean condition |
The table shows that counting() is rarely used alone. Its value comes from being composable with other collectors.
Performance and Runtime Behavior
For most stream pipelines, both Stream.count() and Collectors.counting() consume the entire stream, so their time complexity is O(n). The counting() collector itself does not build an intermediate collection; it is a reducing collector that maps each element to 1L and sums the results. It therefore does not accumulate a list or set.
When you use counting() as a downstream collector inside groupingBy, the grouping operation itself creates a map. That map is the main memory cost. The counting step itself adds only a Long value per group.
For parallel streams, counting() works correctly because it has a combiner that adds the partial counts. The groupingBy collector, however, is not concurrent by default. If you are processing a large stream in parallel and need a concurrent map, use groupingByConcurrent instead.
Map<Integer, Long> countByLength = words.parallelStream() .collect(Collectors.groupingByConcurrent(String::length, Collectors.counting()));
This produces a ConcurrentMap and allows the grouping operation to be performed concurrently.
Common Edge Cases and Pitfalls
An empty stream produces a count of 0L, not null.
Long count = Stream.empty().collect(Collectors.counting()); System.out.println(count); // 0
If you use groupingBy and then ask for a key that does not exist, counts.get(3) returns null. You need to handle that case separately, for example with getOrDefault.
Another edge case is a classifier that returns null. The default groupingBy collector uses a HashMap, so it can store one null key. But groupingByConcurrent uses a ConcurrentHashMap, which does not allow null keys, and a custom map supplier may also reject them. If null keys are possible in a concurrent grouping, filter them out or use a classifier that returns a non-null default.
List<String> words = Arrays.asList("cat", null); words.parallelStream() .collect(Collectors.groupingByConcurrent(Function.identity(), Collectors.counting())); // NPE
When counting() Is the Wrong Choice
If you need the sum of a numeric field rather than the number of elements, use summingLong or summingInt instead of counting(). For example, to sum order totals:
long totalAmount = orders.stream() .collect(Collectors.summingLong(Order::getAmount));
counting() only tells you how many orders there are, not their total value. Similarly, if you need both the count and another aggregate, consider teeing (Java 12+) to compute both in one pass.
class OrderStats { final long count; final long total; OrderStats(long count, long total) { this.count = count; this.total = total; } } OrderStats stats = orders.stream() .collect(Collectors.teeing( Collectors.counting(), Collectors.summingLong(Order::getAmount), OrderStats::new ));
This avoids traversing the stream twice. counting() is the right tool when you need a count as a collector value, not when you need a primitive long from a simple stream.