简体   繁体   中英

Using Java Streams API to dynamically convert a list into a group map with enum key

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?

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()))
    );

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 .

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),
                    a -> {
                        PieSlice p = new PieSlice();
                        p.addAmount(a);
                        return p;
                    }))));
for (var e : sliceMap.entrySet())
    System.out.println(e);

output:

Fizz=PieSlice [label=null, value=300]
Thang=PieSlice [label=null, value=200]
Thing=PieSlice [label=null, value=500]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM