Search code examples
javacollectionshashmapjava-stream

Getting data from Map inside a Map with streams


I have a problem with receiving data from Map nested in another Map.

private Map<Customer, Map<Item,Integer>> orders;

I'm generating this map from JSON, its add Customer if he is not on the list with Items and their number. If Customer is already in the map then key Item in the second map is updated and if a key was there already then Integer which is the number of items is updated. Classes Customer and Items are not connected I mean Class Customer don't have field Items and class Items don't have a field Customer.

public class Customer {

    private String name;
    private String surname;
    private Integer age;
    private BigDecimal money;
}


public class Item {

    private String name;
    private String category;
    private BigDecimal price;
}

Using streams I want to get for example Customer who paid the most for items but I have problem with getting this data from the map, it was not so hard with List but now I can't figure it out.

Ok I did figure out something like this and it seems to be working but I'm sure it can be simplified.

Customer key = customersMap.entrySet()
                .stream()
                .collect(Collectors.toMap(Map.Entry::getKey,
                        e -> e.getValue()
                                .entrySet()
                                .stream()
                                .map(o -> o.getKey().getPrice().multiply(BigDecimal.valueOf(o.getValue())))
                                .collect(Collectors.toList())))
                .entrySet()
                .stream()
                .collect(Collectors.toMap(Map.Entry::getKey, t -> t.getValue().stream().reduce(BigDecimal.ZERO, BigDecimal::add)))
                .entrySet()
                .stream()
                .max(Map.Entry.comparingByValue())
                .orElseThrow()
                .getKey();

Your answer Naman was very helpful so maybe you can give me advice about this. This is how I'm receiving it from JSON.

JsonConverterCustomer jsonConverterCustomer = new JsonConverterCustomer(FILENAME3);
List<Order> orders = jsonConverterCustomer.fromJson().orElseThrow();
 

Map<Customer, Map<Item, Integer>> customersMap = new HashMap<>();

        

for (Order order : orders) {
            if (!customersMap.containsKey(order.getCustomer())) {
                addNewCustomer(customersMap, order);
            } else {
                for (Product product : order.getItems()) {
                    if (!customersMap.get(order.getCustomer()).containsKey(items)) {
                        addNewCustomerItem(item, customersMap.get(order.getCustomer()));
                    } else {
                        updateCustomerItem(customersMap, order, item);
                    }
                }
            }
        }


private static void updateCustomerProduct(Map<Customer, Map<Item, Integer>> customersMap, Order order, Item item) {
        customersMap.get(order.getCustomer())
                .replace(item,
                        customersMap.get(order.getCustomer()).get(item),
                        customersMap.get(order.getCustomer()).get(item) + 1);
    }

    private static void addNewCustomerItem(Item item, Map<Item, Integer> itemIntegerMap) {
        itemIntegerMap.put(item, 1);
    }

    private static void addNewCustomer(Map<Customer, Map<Item, Integer>> customersMap, Order order) {
        Map<Item, Integer> temp = new HashMap<>();
        addNewCustomerItem(order.getItems().get(0), temp);
        customersMap.put(order.getCustomer(), temp);
    }

Order class is a class which one help me receiving data from JSON It is a simple class with Customer as a field and List as a field. As you can see I'm receiving List of Orders and from it, I'm creating this Map. Can I make it more functional? Using streams? I was trying to do but not sure how;/


Solution

  • There are two possible ways to make it more maintainable/readable as Jason pointed out and at the same time simplify the logic performed.

    • One, you can get rid of one of the stages in the pipeline and merge map and reduce into a single pipeline.
    • Another would be to abstract out per customer computation of the total amount paid by them.

    So the abstraction would look like the following and work on the inner maps for your input:

    private BigDecimal totalPurchaseByCustomer(Map<Item, Integer> customerOrders) {
        return customerOrders.entrySet()
                .stream()
                .map(o -> o.getKey().getPrice().multiply(BigDecimal.valueOf(o.getValue())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    

    Now to easily fit this in while you iterate for each customer entry, you can do that in a single collect itself:

    private Customer maxPayingCustomer(Map<Customer, Map<Item, Integer>> customersMap) {
        Map<Customer, BigDecimal> customerPayments = customersMap.entrySet()
                .stream()
                .collect(Collectors.toMap(Map.Entry::getKey,
                        e -> totalPurchaseByCustomer(e.getValue())));
        return customerPayments.entrySet()
                .stream()
                .max(Map.Entry.comparingByValue())
                .map(Map.Entry::getKey)
                .orElseThrow();
    }