Java Stream's methods fit most use-cases. They allow returning either a or a scalar. For the former, you use one of the method, for the latter, one of the one. Collectors Collection toXXX() reducing() Let's imagine an e-commerce platform that implements a shopping cart. The cart is modeled as the following: This diagram might translate into the following (abridged) code: { Long id; String label; BigDecimal price; { .id = id; .label = label; .price = price; } { ... } { ... } } public class Product private final // 1 private final // 1 private final // 1 public Product (Long id, String label, BigDecimal price) this this this @Override public boolean equals (Object object ) // 2 @Override public int hashCode () // 2 Getters Only depend on id { Map<Product, Integer> products = HashMap<>(); { add(product, ); } { products.merge(product, quantity, Integer::sum); } { products.remove(product); } { products.put(product, quantity); } { Collections.unmodifiableMap(products); } } public class Cart private final new // 1 public void add (Product product) 1 public void add (Product product, quantity) int public void remove (Product product) public void setQuantity (Product product, quantity) int Map<Product, Integer> public getProducts () return // 2 Organize products into a map. The key is the ; the value is the quantity. Product Remember to return a read-only copy of the collection to maintain encapsulation. Once we have defined how we store data in memory, we need to design how to display the cart on-screen. We know that the checkout screen needs to show two different bits of information: The list of rows with the price for each row, , the price per product times the quantity. i.e. The overall price of the cart. Here's the corresponding code: { { (entry.getKey(), entry.getValue()); } { product.getPrice().multiply( BigDecimal(quantity)); } } record public CartRow (Product product, quantity) int // 1 public CartRow (Map.Entry<Product, Integer> entry) this BigDecimal public getRowPrice () return new is a value object. We can model it as a Java 16 . CartRow record rows = cart.getProducts() .entrySet() .stream() .map(CartRow:: ) .collect(Collectors.toList()); price = cart.getProducts() .entrySet() .stream() .map(CartRow:: ) .map(CartRow::getRowPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); var new // 1 var new // 2 // 3 Collect the list of rows. Compute the price for each row. Compute the total price. One of the main limitations of Java streams is that . The reason is that streamed objects are not necessarily immutable (though they can be). Hence, executing the same stream twice might not be . you can only consume them once idempotent Therefore, to get both the rows and the price, we need to create two streams from the cart. From one stream, we will get the rows and from the other the price. This is not the way. We want to collect both rows and the price from a single stream. We need a custom that returns both in one pass as a single object. Collector { BigDecimal price; List<CartRow> rows = ArrayList<>(); PriceAndRows(BigDecimal price, List<CartRow> rows) { .price = price; .rows.addAll(rows); } PriceAndRows() { (BigDecimal.ZERO, ArrayList<>()); } } public class PriceAndRows private // 1 private final new // 2 this this this new Total cart price. List of cart rows that can display the product's label, the product's price, and the row price. Here's a summary of the interface. For more details, please check . Collector this previous post : Supply the base object to start from supplier() : Describe how to accumulate the current streamed item to the container accumulator() : If the stream is parallel, describe how to merge them combiner() : If the mutable container type is not the returned type, describe how to transform the former into the latter finisher() : Provide meta-data to optimize the stream | characteristics() Given this, we can implement the accordingly: Collector { { PriceAndRows:: ; } BiConsumer<PriceAndRows, Map.Entry<Product, Integer>> accumulator() { (priceAndRows, entry) -> { row = CartRow(entry); priceAndRows.price = priceAndRows.price.add(row.getRowPrice()); priceAndRows.rows.add(row); }; } { (c1, c2) -> { c1.price = c1.price.add(c2.price); rows = ArrayList<>(c1.rows); rows.addAll(c2.rows); PriceAndRows(c1.price, rows); }; } { Function.identity(); } { Set.of(Characteristics.IDENTITY_FINISH); } } private < . < , >, , > class PriceAndRowsCollector implements Collector Map Entry Product Integer PriceAndRows PriceAndRows @Override Supplier<PriceAndRows> public supplier () return new // 1 @Override public return // 2 var new @Override BinaryOperator<PriceAndRows> public combiner () return // 3 var new return new @Override Function<PriceAndRows, PriceAndRows> public finisher () return // 4 @Override Set<Characteristics> public characteristics () return // 4 The mutable container is an instance of . PriceAndRows For each map entry containing the product and the quantity, accumulate both into the . PriceAndRows Two can be combined by summing their total price and aggregating their respective rows. PriceAndRows The mutable container can be returned as-is. Designing the is a bit involved, but using the custom collector is as easy as: Collector priceAndRows = cart.getProducts() .entrySet() .stream() .collect( PriceAndRowsCollector()); var new Conclusion You can solve most use cases with one of the out-of-the-box collectors provided in the class. However, some require to implement a custom , , when you need to collect more than a single collection or a single scalar. Collectors Collector e.g. While it may seem complicated if you never developed one before, it's not. You only need a bit of practice. I hope this post might help you with it. You can find the source code of this post on in Maven format. GitHub To go further Custom collectors in Java 8 Collector Javadocs Originally published at on May 2nd, 2021 A Java Geek Previously published at https://blog.frankel.ch/real-world-stream-collector/