Summing method in Collectors API
-
Last Updated: April 11, 2024
-
By: javahandson
-
Series
Learn Java in a easy way
The summing method in Collectors API adds up one numeric property across a whole stream of objects. This guide covers summingInt, summingLong, and summingDouble with examples, the silent overflow trap, per-group totals, and interview questions.
Adding numbers is the first thing anyone learns to program. Then you meet a list of objects, and suddenly it gets fiddly again.
You do not want to add the objects. You want to add one field inside them. Every student has marks, every order has an amount, every file has a size, and you need the total of just that one field.
The old way declares a variable, loops, and accumulates. It works fine, and it takes four lines to say something very simple.
Java 8 offers a one-line answer. Hand a summing collector a method reference, and it pulls that field out of every element and totals it for you.
// The loop
int total = 0;
for (Student student : studentList) {
total += student.getMarks();
}
// The collector
int total = studentList.stream()
.collect(Collectors.summingInt(Student::getMarks));Java gives you three of these, one per numeric type. They look almost identical, and one of them will silently give you a wrong answer if you pick it carelessly. Let us see why.
We work through each variant, then look at where these collectors genuinely beat a plain loop. Here is the plan:
You need a little stream experience to follow along. Our guides on lambda expressions and method references cover the syntax we lean on.
Three methods, one job. Each pulls a number out of every element and adds them all together.
Picture a stack of exam papers. You flip through them one at a time, read the mark in the corner, and keep a running total in your head. At the end you have one number.
That is the whole idea. The collector never looks at the rest of the object. It calls your extractor, takes the number it gets back, and adds it to the running total.
The extractor is the part you supply. Write it as a method reference such as Student::getMarks, or as a lambda such as s -> s.getMarks(). Both read the same field.
All three sit in java.util.stream.Collectors, beside toList, groupingBy, and the other factory methods.
Our introduction to the Collectors class lays out the wider family.
Here is a detail that trips people up later. A Collector must produce an object, because generics cannot hold a primitive. So these three return Integer, Long, and Double, not int, long, and double.
// Collectors.summingInt gives you an Integer
Integer boxed = studentList.stream()
.collect(Collectors.summingInt(Student::getMarks));
// Assigning to int works, because Java unboxes it for you
int plain = studentList.stream()
.collect(Collectors.summingInt(Student::getMarks));Most of the time you never notice, since Java unboxes automatically. The wrapper does bite in one specific place, and section 10.1 shows exactly where.
Learn the shape once and all three follow. Only the numeric type moves.
| Method | Mapper | Returns | Empty stream |
|---|---|---|---|
| summingInt | ToIntFunction | Integer | 0 |
| summingLong | ToLongFunction | Long | 0 |
| summingDouble | ToDoubleFunction | Double | 0.0 |
That last column is a small kindness. An empty stream gives you a clean zero, so no Optional and no null check.
Matching the variant to your getter is the obvious start. Thinking about the total is the part people skip.
Notice the second point. The type of your field is not the only question. The size of the answer matters just as much.
Start here, since the other two are the same method with one word changed.
public static <T> Collector<T, ?, Integer>
summingInt(ToIntFunction<? super T> mapper)Four pieces, and none of them is hard on its own.
Begin with public static. The method belongs to the class, so you always write Collectors.summingInt(…).
Then <T> stands for whatever your stream carries. Stream a list of Student objects and T becomes Student.
The return type is Collector<T, ?, Integer>, which declares three types of its own:
Finally, ToIntFunction<? super T> mapper is your extractor. It takes one object and returns a plain int. The ? super T part simply lets an extractor written for a parent class work on a stream of its children.
package com.javahandson.collectors.summing;
public class Student {
int rollNumber;
String name;
int marks;
public Student(int rollNumber, String name, int marks) {
this.rollNumber = rollNumber;
this.name = name;
this.marks = marks;
}
public String getName() {
return name;
}
public int getMarks() {
return marks;
}
}package com.javahandson.collectors.summing;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class SummingIntDemo {
public static void main(String[] args) {
List<Student> studentList = Arrays.asList(
new Student(101, "Suraj", 450),
new Student(102, "Iqbal", 470),
new Student(103, "Amar", 430),
new Student(104, "Amit", 400),
new Student(105, "Suchit", 380),
new Student(106, "Kartik", 490));
int totalMarks = studentList.stream()
.collect(Collectors.summingInt(Student::getMarks));
System.out.println("Total marks of students : " + totalMarks);
}
}
// Output:
// Total marks of students : 2620Check the arithmetic if you like. 450 plus 470 plus 430 plus 400 plus 380 plus 490 comes to 2620.
The collector touched each student once and never built an intermediate list. That is the same amount of work the hand-written loop does.
Run the same code over an empty list and nothing breaks.
List<Student> studentList = new ArrayList<>();
int totalMarks = studentList.stream()
.collect(Collectors.summingInt(Student::getMarks));
System.out.println("Total marks of students : " + totalMarks);
// Output:
// Total marks of students : 0No exception, no Optional, no null. Zero is the right answer for an empty total, and the collector just gives it to you.
This is a genuine advantage over some of its neighbours. A max or a min has no sensible answer for an empty stream, which is why those return an Optional instead.
Same collector, wider numbers. This is the one to reach for when totals get big.
public static <T> Collector<T, ?, Long>
summingLong(ToLongFunction<? super T> mapper)Set the two signatures side by side and only two words differ.
Typical long fields include database ids, file sizes in bytes, epoch timestamps, and view counters.
Here is the useful part. A ToLongFunction happily accepts a getter that returns an int, because Java widens int to long automatically.
// getMarks() returns int, yet summingLong accepts it
long totalMarks = studentList.stream()
.collect(Collectors.summingLong(Student::getMarks));
System.out.println(totalMarks);
// Output:
// 2620So you can keep your int field and still get a long total. That single swap removes any chance of overflow, and it costs you nothing.
Remember this trick. It is the cleanest fix for the problem section 7 is about to describe.
The third variant handles anything with a decimal point: prices, rates, weights, and percentages.
public static <T> Collector<T, ?, Double>
summingDouble(ToDoubleFunction<? super T> mapper)It accepts int and long getters as well, since Java widens both to double. The shape should feel familiar by now.
package com.javahandson.collectors.summing;
public class Item {
private final String name;
private final String category;
private final double price;
public Item(String name, String category, double price) {
this.name = name;
this.category = category;
this.price = price;
}
public String getName() { return name; }
public String getCategory() { return category; }
public double getPrice() { return price; }
}package com.javahandson.collectors.summing;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class SummingDoubleDemo {
public static void main(String[] args) {
List<Item> items = Arrays.asList(
new Item("Notebook", "Stationery", 120.50),
new Item("Pen", "Stationery", 15.25),
new Item("Bag", "Accessories", 899.00),
new Item("Bottle", "Accessories", 249.75),
new Item("Lamp", "Home", 515.50));
double total = items.stream()
.collect(Collectors.summingDouble(Item::getPrice));
System.out.println("Cart total : " + total);
System.out.printf("Formatted : %.2f%n", total);
}
}
// Output:
// Cart total : 1800.0
// Formatted : 1800.00Look at the two output lines. Printing the raw double gives 1800.0, because Java drops trailing zeros. A price on a screen wants printf or a DecimalFormat instead.
Adding many doubles loses precision. Each addition rounds off a tiny amount, and those crumbs pile up over thousands of values.
summingDouble pushes back with compensated summation. It tracks the error left behind by each addition and folds it in at the end, so the total drifts far less than a naive running += would.
It cannot work miracles, though. The javadoc warns that the result still depends on the order of the values, so two runs over differently sorted data can differ in the last digits.
One bad value defeats it outright. Feed in a single NaN and the entire total becomes NaN, with no partial recovery.
// Filter suspect values before they reach the collector
double safe = items.stream()
.filter(item -> !Double.isNaN(item.getPrice()))
.collect(Collectors.summingDouble(Item::getPrice));This section is the one to remember. It produces wrong numbers rather than exceptions, which makes it far more dangerous than a crash.
An int stops at 2147483647. Add one more and it does not throw. It wraps around to the most negative value and keeps going.
Inside, summingInt accumulates into a plain int. So the moment your running total crosses that line, the answer turns negative and nothing warns you.
package com.javahandson.collectors.summing;
public class LargeNumber {
private final int number;
public LargeNumber(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
}package com.javahandson.collectors.summing;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class OverflowDemo {
public static void main(String[] args) {
List<LargeNumber> list = Arrays.asList(
new LargeNumber(2147483647), // Integer.MAX_VALUE
new LargeNumber(10));
int intSum = list.stream()
.collect(Collectors.summingInt(LargeNumber::getNumber));
long longSum = list.stream()
.collect(Collectors.summingLong(LargeNumber::getNumber));
double doubleSum = list.stream()
.collect(Collectors.summingDouble(LargeNumber::getNumber));
System.out.println("summingInt : " + intSum);
System.out.println("summingLong : " + longSum);
System.out.println("summingDouble : " + doubleSum);
}
}
// Output:
// summingInt : -2147483639
// summingLong : 2147483657
// summingDouble : 2.147483657E9The first line is the whole lesson. Two positive numbers added together produced a negative result, and Java reported no error at all.
Work out where -2147483639 comes from. The true total is 2147483657, which sits 10 past the int ceiling. Wrapping subtracts the full int range and lands you deep in negative territory.
The other two lines handle it. A long has room to spare, and a double has range to spare. Java prints the double in scientific notation because the value passed ten million.
A simple habit avoids the whole problem. Ask how large the total could get, not how large one element is.
Choosing summingLong for an int field costs nothing measurable. Choosing summingInt and overflowing costs you a wrong report that nobody spots for months.
For a single total, a plain loop is honestly just as clear. The collectors earn their place when you nest them.
Pass a summing collector as the downstream and every group gets its own total, from one pass over the data.
Map<String, Double> byCategory = items.stream()
.collect(Collectors.groupingBy(Item::getCategory,
Collectors.summingDouble(Item::getPrice)));
System.out.println("Stationery : " + byCategory.get("Stationery"));
System.out.println("Accessories : " + byCategory.get("Accessories"));
System.out.println("Home : " + byCategory.get("Home"));
// Output:
// Stationery : 135.75
// Accessories : 1148.75
// Home : 515.5Try writing that with loops. You would need a map, a lookup, a null check, and an accumulate step, all inside the loop body.
Stationery adds 120.50 and 15.25 to reach 135.75. Accessories adds 899.00 and 249.75 for 1148.75. Home holds one item, so it prints 515.5 with the trailing zero dropped. Our guide on grouping in Java 8 covers the classifier side.
When the split is a plain yes or no, partitioningBy reads better and guarantees both keys exist.
Map<Boolean, Double> byPrice = items.stream()
.collect(Collectors.partitioningBy(
item -> item.getPrice() >= 250,
Collectors.summingDouble(Item::getPrice)));
System.out.println("Premium : " + byPrice.get(true));
System.out.println("Everyday: " + byPrice.get(false));
// Output:
// Premium : 1414.5
// Everyday: 385.5The Bag at 899.00 and the Lamp at 515.50 clear the 250 line, giving 1414.5. Everything else totals 385.5. Note that the Bottle at 249.75 misses the cut by a quarter of a rupee.
Because partitioningBy always fills both keys, neither lookup can return null. Our article on partitioning in Java 8 explains why that matters.
A map of totals is half a report. Usually you also want them ordered, biggest first.
byCategory.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.forEach(entry -> System.out.printf("%-12s %.2f%n",
entry.getKey(), entry.getValue()));
// Output:
// Accessories 1148.75
// Home 515.50
// Stationery 135.75The comparingByValue helper sorts on the total, and reversed flips it to descending. Swap in comparingByKey to sort alphabetically instead.
The summing collectors are not the only way to total a field. Three alternatives come up constantly.
Primitive streams carry their own sum() method. Map first, then add.
// Route A: the collector, returns Integer
int viaCollector = studentList.stream()
.collect(Collectors.summingInt(Student::getMarks));
// Route B: the primitive stream, returns int
int viaStream = studentList.stream()
.mapToInt(Student::getMarks)
.sum();
// Both print 2620Route B is shorter and skips the boxing, so prefer it for a standalone total. Route A wins the moment you need to nest the total inside groupingBy, because only a Collector fits that slot.
Needing the total and the average is common. Running two collectors means two passes, so use one summarizing collector instead.
IntSummaryStatistics stats = studentList.stream()
.collect(Collectors.summarizingInt(Student::getMarks));
System.out.println(stats.getSum()); // 2620 (a long, not an int)
System.out.println(stats.getAverage()); // 436.6666666666667
System.out.println(stats.getMax()); // 490One extra benefit hides in there. The getSum() method returns a long, so a summarizing collector cannot overflow the way summingInt can. Our guide on Java 8 summarizing methods covers the family in full.
There is no summingBigDecimal, and money really should not live in a double. The reducing collector fills that gap.
// A plain total
BigDecimal total = invoices.stream()
.map(Invoice::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// The same thing as a downstream collector
Map<String, BigDecimal> byClient = invoices.stream()
.collect(Collectors.groupingBy(Invoice::getClient,
Collectors.reducing(BigDecimal.ZERO,
Invoice::getAmount,
BigDecimal::add)));The three arguments read as a sentence: start at zero, pull this field out, and combine two values this way. Our article on the reduce method goes deeper.
| Approach | Returns | Boxes? | Best for |
|---|---|---|---|
| Collectors.summingInt | Integer | Yes | Totals inside groupingBy |
| mapToInt().sum() | int | No | A standalone total |
| Collectors.summarizingInt | IntSummaryStatistics | No | Two or more figures |
| Collectors.reducing | Your own type | n/a | BigDecimal and custom types |
Six mistakes account for most of the summing bugs we meet in code reviews.
Remember that summing collectors return wrappers. Combine that with groupingBy, which omits keys that matched nothing, and you get a classic crash.
Map<String, Double> byCategory = items.stream()
.collect(Collectors.groupingBy(Item::getCategory,
Collectors.summingDouble(Item::getPrice)));
// No Books category exists, so get returns null, and unboxing it throws
double books = byCategory.get("Books"); // NullPointerException
// Safe
double safeBooks = byCategory.getOrDefault("Books", 0.0); // 0.0The exception points at an innocent-looking assignment, which makes it confusing to debug. Use getOrDefault whenever the key might be missing.
Section 7 covered this, and it deserves the repeat because it ships so quietly. Test data is small, so the bug never appears until real volumes arrive.
When in doubt, use summingLong. The cost is nothing and the protection is total.
A double cannot hold every decimal exactly. Add 0.1 ten times and you do not land on 1.0, and those gaps show up on an invoice.
Compensated summation reduces the drift but never removes it. For anything a customer pays, use BigDecimal with reducing, or store paise and cents as a long and use summingLong.
Stream<Student> stream = studentList.stream(); int marks = stream.collect(Collectors.summingInt(Student::getMarks)); // IllegalStateException: stream has already been operated upon or closed int again = stream.collect(Collectors.summingInt(Student::getMarks));
A stream works exactly once. Call studentList.stream() freshly each time, since the source list itself stays reusable.
The collector never produces a null total. Your extractor can still explode, though, if an element itself is null.
// Throws NullPointerException inside getMarks()
list.stream().collect(Collectors.summingInt(Student::getMarks));
// Filter first
list.stream()
.filter(Objects::nonNull)
.collect(Collectors.summingInt(Student::getMarks));Chaining three separate collectors for a total, a count, and an average walks the data three times.
One summarizingInt gives you all five figures in a single pass. Reach for the summing collectors only when the total is genuinely all you want.
Let us build one small report that uses two variants together.
We have the lines of a shopping cart. Each line carries a category, a quantity, and an amount. The report needs:
Quantities are whole numbers and money has decimals, so this needs summingInt and summingDouble together.
package com.javahandson.collectors.summing;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class CartReport {
static class Line {
private final String category;
private final int quantity;
private final double amount;
Line(String category, int quantity, double amount) {
this.category = category;
this.quantity = quantity;
this.amount = amount;
}
public String getCategory() { return category; }
public int getQuantity() { return quantity; }
public double getAmount() { return amount; }
}
public static void main(String[] args) {
List<Line> lines = Arrays.asList(
new Line("Stationery", 3, 135.75),
new Line("Accessories", 1, 899.00),
new Line("Accessories", 2, 249.75),
new Line("Home", 1, 515.50),
new Line("Stationery", 5, 76.25));
long totalUnits = lines.stream()
.collect(Collectors.summingLong(Line::getQuantity));
double orderValue = lines.stream()
.collect(Collectors.summingDouble(Line::getAmount));
Map<String, Integer> unitsByCategory = lines.stream()
.collect(Collectors.groupingBy(Line::getCategory,
Collectors.summingInt(Line::getQuantity)));
Map<String, Double> valueByCategory = lines.stream()
.collect(Collectors.groupingBy(Line::getCategory,
Collectors.summingDouble(Line::getAmount)));
System.out.println("Lines : " + lines.size());
System.out.println("Total units : " + totalUnits);
System.out.printf("Order value : %.2f%n", orderValue);
for (String category : Arrays.asList("Stationery", "Accessories", "Home")) {
System.out.println(category
+ " : qty " + unitsByCategory.getOrDefault(category, 0)
+ ", " + String.format("%.2f",
valueByCategory.getOrDefault(category, 0.0)));
}
}
}
// Output:
// Lines : 5
// Total units : 12
// Order value : 1876.25
// Stationery : qty 8, 212.00
// Accessories : qty 3, 1148.75
// Home : qty 1, 515.50Check the totals against the data. Quantities 3, 1, 2, 1, and 5 add up to 12. The amounts come to 1876.25, and the three category values of 212.00, 1148.75, and 515.50 add back to the same figure.
Three habits from earlier sections appear in this code. Quantities use summingLong even though the field is an int, so a large cart can never overflow.
Every map lookup goes through getOrDefault, so a missing category returns a zero rather than throwing on unboxing. And String.format handles the money, because the raw double would print 212.0 instead of 212.00.
Swap any one of those and the report either crashes or looks wrong to a customer.
A: It is a family of three collectors in java.util.stream.Collectors that add up one numeric property across a stream of objects. You pass an extractor such as Student::getMarks, and the collector returns the total of that field.
A: summingInt takes a ToIntFunction and returns Integer, summingLong takes a ToLongFunction and returns Long, and summingDouble takes a ToDoubleFunction and returns Double. Java 8 shipped all three, and their signatures have not changed since.
A: It returns 0, not null and not an Optional. summingLong also returns 0, and summingDouble returns 0.0. That makes them safer than max or min, which have no sensible answer for an empty stream and hand back an Optional instead.
A: A Collector must produce an object, because Java generics cannot hold a primitive type. Assigning the result to an int still works, since Java unboxes automatically. The wrapper only causes trouble when a map lookup returns null and unboxing throws a NullPointerException.
A: It wraps silently and gives a wrong answer, with no exception. Adding 10 to Integer.MAX_VALUE returns -2147483639. Use summingLong instead, which accepts an int getter through automatic widening and has room for a far larger total.
A: Both produce the same total in one pass. mapToInt(…).sum() returns a primitive int and skips the boxing, so it reads better for a standalone total. Collectors.summingInt is the only option when you need the total as a downstream collector inside groupingBy or partitioningBy.
A: Yes. A ToLongFunction accepts a getter returning an int, because Java widens int to long automatically. This is the simplest way to keep an int field while getting a total that cannot overflow. The same widening lets summingDouble accept int and long getters.
A: Pass a summing collector as the downstream argument to groupingBy. For example groupingBy(Item::getCategory, summingDouble(Item::getPrice)) returns a Map of category to total. Use partitioningBy instead when the split is a simple yes or no.
A: summingInt gives you only the total, as an Integer that can overflow. summarizingInt gives you count, sum, min, max, and average in one IntSummaryStatistics object, and its sum is a long. Use summingInt when the total is all you need.
A: Not for amounts a customer pays. A double cannot represent every decimal exactly, and the errors accumulate. Use BigDecimal with Collectors.reducing(BigDecimal.ZERO, Invoice::getAmount, BigDecimal::add), or store the smallest unit such as paise as a long and use summingLong.
A: groupingBy only creates a key when something matched it, so a missing key returns null. Because summingInt produces an Integer, assigning that null to an int unboxes it and throws. Use getOrDefault(key, 0) instead.
Let us wrap up what we covered. The summing collectors add one numeric property across a stream, and Java gives you three of them, one per numeric type.
All three return wrapper objects rather than primitives, and all three answer an empty stream with a clean zero. No Optional, no null, no special case in your code.
Watch the overflow. A summingInt total wraps silently once it passes 2147483647, and summingLong costs nothing while removing that risk entirely. It even accepts your existing int getter.
Keep summingDouble away from real money. Reach for BigDecimal with reducing, or count in paise with a long.
Above all, remember where these collectors beat a loop. Nested inside groupingBy or partitioningBy, they turn a page of map handling into one readable line.