javajava-streamclasscastexceptioncollectaccumulator

How to achieve an quantity summed up object list where the name of object is same in java stream collect operation avoiding toMap


I'm trying to get the quantity of the object summed up if the name of the object matches.

The POJO details are as below:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class InventoryDetail {
    private String name;
    private int quantity;
}

The code is as follows:

public class InventoryDetailsCollationUpdateQuantityForSameName {
    public static void main(String[] args) {
        List<InventoryDetail> inventoryDetails = Arrays.asList(
                new InventoryDetail("iPhone", 1),
                new InventoryDetail("Samsung", 1),
                new InventoryDetail("iPhone", 2),
                new InventoryDetail("Motorolla", 1),
                new InventoryDetail("Nokia", 1),
                new InventoryDetail("iPhone", 1)
        );

        Function<Object, InventoryDetail> objectToInventorySkuDetailsFunction = o -> (InventoryDetail) o;

        //This works but want to know the other way to do with the .collect
        List<InventoryDetail> listFromToMap = inventoryDetails.stream()
            .map(objectToInventorySkuDetailsFunction)
            .collect(Collectors.toMap(InventoryDetail::getName,
                Function.identity(),
                (is1, is2) -> {
                    is1.setQuantity(is1.getQuantity() + is2.getQuantity());
                    return is1;
                }))
            .entrySet().stream()
            .map(stringInventoryDetailEntry -> stringInventoryDetailEntry.getValue())
            .collect(Collectors.toList());
        System.out.println(listFromToMap);

        ArrayList<Object> listFromCollect = inventoryDetails.stream().collect(ArrayList::new,
            (list, newInventorySkuDetail) -> {
                if (list.isEmpty() || list.stream().map(objectToInventorySkuDetailsFunction)
                        .noneMatch(existingInventorySkuDetail -> StringUtils.equalsIgnoreCase(existingInventorySkuDetail
                            .getName(), newInventorySkuDetail.getName()))) {
                    list.add(inventoryDetails);
                } else {
                    list.stream()
                        .map(objectToInventorySkuDetailsFunction)
                        .filter(existingInventorySkuDetail -> StringUtils.equalsIgnoreCase(existingInventorySkuDetail
                            .getName(), newInventorySkuDetail.getName()))
                        .forEach(existingInventorySkuDetail -> existingInventorySkuDetail
                            .setQuantity(existingInventorySkuDetail.getQuantity() + newInventorySkuDetail.getQuantity()));
                }
            }
            , (l1, l2) -> {
                l2.stream()
                    .map(objectToInventorySkuDetailsFunction)
                    .forEach(inventorySkuDetailToBeChecked -> {
                        if (l1.stream().map(objectToInventorySkuDetailsFunction)
                                .anyMatch(areInventorySkuDetailSame(inventorySkuDetailToBeChecked))) {
                            l1.stream()
                                .map(objectToInventorySkuDetailsFunction)
                                .filter(areInventorySkuDetailSame(inventorySkuDetailToBeChecked))
                                .forEach(existingInventorySkuDetail -> existingInventorySkuDetail
                                    .setQuantity(existingInventorySkuDetail.getQuantity() + inventorySkuDetailToBeChecked.getQuantity()));
                        }
                    });
            }
        );
        System.out.println(listFromCollect);
    }

    private static Predicate<InventoryDetail> areInventorySkuDetailSame(InventoryDetail newInventoryDetail) {
        return existingInventoryDetail -> StringUtils.equalsIgnoreCase(existingInventoryDetail.getName(), newInventoryDetail.getName());
    }

    private static Consumer<InventoryDetail> addToQuantityAsNeeded(InventoryDetail inventoryDetailToBeChecked) {
        return existingInventoryDetail -> existingInventoryDetail.setQuantity(existingInventoryDetail.getQuantity() + inventoryDetailToBeChecked.getQuantity());
    }
}

The expected output is as below from listFromToMap, but I need to get it in listFromCollect. Please help me understand what the issue is

[InventoryDetail(name=Apple, quantity=4), InventoryDetail(name=Samsung, quantity=1), InventoryDetail(name=Nokia, quantity=1), InventoryDetail(name=Motorola, quantity=1)]

When trying to run the above code I get the below error

Exception in thread "main" java.lang.ClassCastException: java.util.Arrays$ArrayList cannot be cast to com.learning.java.pojo.InventoryDetail
    at com.learning.java.eight.stream.InventoryDetailsCollationUpdateQuantityForSameName.lambda$main$0(InventoryDetailsCollationUpdateQuantityForSameName.java:23)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1359)
    at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126)
    at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:499)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:486)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
    at java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:230)
    at java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:196)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
    at com.learning.java.eight.stream.InventoryDetailsCollationUpdateQuantityForSameName.lambda$main$6(InventoryDetailsCollationUpdateQuantityForSameName.java:42)
    at java.util.stream.ReduceOps$4ReducingSink.accept(ReduceOps.java:220)
    at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:510)
    at com.learning.java.eight.stream.InventoryDetailsCollationUpdateQuantityForSameName.main(InventoryDetailsCollationUpdateQuantityForSameName.java:39)

Please help me with a clean way to get the list of InventtorySku with summed up quantity if the name is the same.


Solution

  • You seem to have made a typo in the if branch of the accumulator function:

    list.add(inventoryDetails);
    

    should be

    list.add(newInventorySkuDetail);
    

    Otherwise you would be adding an Arrays.ArrayList to the list, but you expect the list to only contain InventoryDetails . That's why the ClassCastException is thrown.

    If you had added the correct thing, you don't need to cast at all. You can delete objectToInventorySkuDetailsFunction entirely.

    Your combiner function is also incorrect. You are supposed to merge the entirety of l2 into l1, but you are ignoring all the items in l2 that does not also exist in l1.

    In any case, for learning purposes, I would collect to a Map instead of a List. It is easier to write the accumulator and the combiner (you can just use merge).

    BiFunction<InventoryDetail, InventoryDetail, InventoryDetail> merge = (detail1, detail2) -> 
            new InventoryDetail(detail1.getName(), detail1.getQuantity() + detail2.getQuantity());
    
    Collection<InventoryDetail> listFromCollect = inventoryDetails.stream().collect(
            HashMap<String, InventoryDetail>::new,
            (map, detail) -> map.merge(detail.getName(), detail, merge),
            (m1, m2) -> m2.forEach((name, detail) -> m1.merge(name, detail, merge))
    ).values();
    

    Notice that both of your approaches changes the existing InventoryDetail objects. I changed that in the implementation above to create new InventoryDetail objects instead.

    Also note that your code using toMap doesn't need the second stream. You can get the values of the map using values():

    BinaryOperator<InventoryDetail> merge = (detail1, detail2) ->
            new InventoryDetail(detail1.getName(), detail1.getQuantity() + detail2.getQuantity());
    Collection<InventoryDetail> listFromToMap = inventoryDetails.stream()
            .collect(Collectors.toMap(
                    InventoryDetail::getName,
                    Function.identity(),
                    merge))
            .values();
    

    If you need to convert it to a List, simply do new ArrayList<>(listFromToMap).