0

Java 8 Streams here. I have the following classes:

public enum Category {
    Thing,
    Thang,
    Fizz
}

@Data // using lombok to generate ctors/getters/setters/etc.
public class LineItem {

    private Long id;
    private String name;
    private Category category;
    private BigDecimal amount;

}

@Data
public class PieSlice {

    private String label;
    private BigDecimal value = BigDecimal.ZERO;

    public void addAmount(BigDecimal amount) {
        value = value.add(amount);
    }

}

In my code I am given a List<LineItem> and I want to convert it to a Map<Category,PieSlice> using the Streams API, if at all possible.

Using the non-Stream way, the conversion would look like:

List<LineItem> lineItems = getSomehow();
Map<Category,PieSlice> sliceMap = new HashMap<>();

PieSlice thingSlice = new PieSlice();
PieSlice thangSlice = new PieSlice();
PieSlice fizzSlice = new PieSlice();

for (LineItem lineItem : lineItems) {
    
    if (lineItem.getCategory().equals(Category.Thing)) {
        thingSlice.addAmount(lineItem.getAmount());
    } else if (lineItem.getCategory().equals(Category.Thang)) {
        thangSlice.addAmount(lineItem.getAmount());
    } else if (lineItem.getCategory().equals(Category.Fizz)) {
        fizz.addAmount(lineItem.getAmount());
    } else {
        throw new RuntimeException("uncategorized line item");
    }

}

sliceMap.put(Category.Thing, thingSlice);
sliceMap.put(Category.Thang, thangSlice);
sliceMap.put(Category.Fizz, fizzSlice);

The problem is that I need to edit the code every time I add a new Category. Is there a way to do this via the Streams API, regardless of what Category values exist?

2
  • You don't need to use Streams to avoid this. Is your primary concern to use Streams, or to avoid coding to each Category? Commented Sep 21, 2022 at 20:52
  • The categories are typically streamed so the main concern is duplication Commented Sep 22, 2022 at 14:04

3 Answers 3

1

Try this.

List<LineItem> lineItems = List.of(
    new LineItem(1L, "", Category.Thing, BigDecimal.valueOf(100)),
    new LineItem(2L, "", Category.Thang, BigDecimal.valueOf(200)),
    new LineItem(3L, "", Category.Fizz, BigDecimal.valueOf(300)),
    new LineItem(4L, "", Category.Thing, BigDecimal.valueOf(400))
);
Map<Category, PieSlice> sliceMap = lineItems.stream()
    .collect(
        groupingBy(LineItem::getCategory,
            mapping(LineItem::getAmount,
                collectingAndThen(
                    reducing(BigDecimal.ZERO, BigDecimal::add),
                    amount -> {
                        PieSlice pieSlice = new PieSlice();
                        pieSlice.addAmount(amount);
                        return pieSlice;
                    }))));
sliceMap.entrySet().stream()
    .forEach(System.out::println);

output:

Fizz=PieSlice [label=null, value=300]
Thang=PieSlice [label=null, value=200]
Thing=PieSlice [label=null, value=500]
Sign up to request clarification or add additional context in comments.

Comments

1

You can use the collect operation to achieve this

        Map<Category, PieSlice> sliceMap = lineItems
                .stream()
                .collect(
                        Collectors.groupingBy(
                                LineItem::getCategory,
                                Collectors.reducing(
                                        new PieSlice(),
                                        item -> {
                                            PieSlice slice = new PieSlice();
                                            slice.addAmount(item.getAmount());
                                            return slice;
                                        },
                                        (slice, anotherSlice) -> {
                                            slice.addAmount(anotherSlice.getValue());
                                            return slice;
                                        }
                                )
                        )
                );

What this piece of code does is a 2-step reduction. First, we take lineItems and group them by their category - reducing the initial list to a map, we achieve this by using Collectors.groupingBy. If we were to use this collector without the second argument, the result would be of type Map<Category, List<LineItem>>. Here is where the Collectors.reducing reducer comes to play - it takes the list of LineItems which are already grouped by their category and turns them into a singular PieSlice, where the original values are accumulated.

You can read more on reduction operations and the standard reducers provided by the JDK here.

Comments

0

The problem is that I need to edit the code every time I add a new Category. Is there a way to do this via the Streams API, regardless of what Category values exist?

You can obtain all declared enum-constants using either values() or EnumSet.allOf(Class<E>).

If you need the resulting map to contain the entry for every existing Category-member, you can provide a prepopulated map through the supplier of collect() operation.

Here's how it might be implemented:

Map<Category, PieSlice> sliceMap = lineItems.stream()
    .collect(
        () -> EnumSet.allOf(Category.class).stream()
            .collect(Collectors.toMap(Function.identity(), c -> new PieSlice())),
        (Map<Category, PieSlice> map, LineItem item) -> 
            map.get(item.getCategory()).addAmount(item.getAmount()),
        (left, right) -> 
            right.forEach((category, slice) -> left.get(category).addAmount(slice.getValue()))
    );

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.