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?
CodePudding user response:
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 whatCategory
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()))
);
CodePudding user response:
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.
CodePudding user response:
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]