Search code examples
javajava-8functional-programmingjava-streamfunctional-java

Use the Stream API to create List from HashMap with elements arranged in specific order


I have a HashMap<String, List<Appliance>> where the field name::String from the object Appliance is used as a key, and each value in the HashMap is a list of Appliance objects. Each list, is sorted in ascending order, based on the field "price::BigDecimal", of the Appliance object. I would like to create an ArrayList<Appliance>, using the Stream API, and prexisted HashMap by extracting, first the first elements of each list in the HashMap, then the second ones, etc. So if the HashMap has these contents:

["Fridge",     [<"Fridge", 100>, <"Fridge", 200>, <"Fridge", 300>],
 "Oven",       [<"Oven", 150>, <"Oven", 250>, <"Oven", 350>],
 "DishWasher", [<"DishWasher", 220>, <"DishWasher", 320>, <"DishWasher", 420>]]

I would like the final list to be as below:

[<"Fridge",     100>,
 <"Oven",       150>,
 <"DishWasher", 220>,
 <"Fridge",     200>,
 <"Oven",       250>,
 <"DishWasher", 320>,
 <"Fridge",     300>,
 <"Oven",       350>,
 <"DishWasher", 420>]

Is it possible to do that in a functional way using Java's 8 Stream API?

This is my code. I would like to achieve the same result in a declarative way.

while(!appliancesMap.isEmpty()) {
    for (Map.Entry<String, List<Appliance>> entry : 
        appliancesMap.entrySet()) {
        String key = entry.getKey();
        List<Appliance> value = entry.getValue();
        finalList.add(value.get(0));
        value.remove(0);
        if (value.size() == 0) {
            appliancesMap.entrySet()
                .removeIf(predicate -> predicate.getKey().equals(key));
        } else {
            appliancesMap.replace(key, value);
        }
    }
}

Solution

  • Steps:

    1. Find the size of the longest list inside the map. This can be done as
    map.keySet().stream().mapToInt(k -> map.get(k).size()).max().getAsInt()
    
    1. Use an IntStream to iterate with the values from 0 to maximum size obtained in step#1
    IntStream.range(0, map.keySet().stream().mapToInt(k -> map.get(k).size()).max().getAsInt())
    
    1. Use each value (say, i) of the IntStream as the index to get the element from the list e.g. if i = 0, get the element at index, 0 from each list inside the map and add to result list
    List<Appliance> result = new ArrayList<>();
    
    IntStream.range(0, map.keySet().stream().mapToInt(k -> map.get(k).size()).max().getAsInt())
        .forEach(i -> map
                    .keySet()
                    .stream()
                    .filter(key -> i < map.get(key).size())
                    .forEach(k -> result.add(map.get(k).get(i))));
    

    Demo

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.IntStream;
    
    class Appliance {
        private String name;
        private double price;
    
        public Appliance(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
        @Override
        public String toString() {
            return "Appliance [name=" + name + ", price=" + price + "]";
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Map<String, List<Appliance>> map = Map.of("Fridge",
                    List.of(new Appliance("Fridge", 100), new Appliance("Fridge", 200), new Appliance("Fridge", 300)),
                    "Oven", List.of(new Appliance("Oven", 150), new Appliance("Oven", 250), new Appliance("Oven", 350)),
                    "DishWasher", List.of(new Appliance("DishWasher", 220), new Appliance("DishWasher", 320),
                            new Appliance("DishWasher", 420)));
    
            List<Appliance> result = new ArrayList<>();
    
            IntStream.range(0, map.keySet().stream().mapToInt(k -> map.get(k).size()).max().getAsInt())
            .forEach(i -> map
                    .keySet()
                    .stream()
                    .filter(key -> i < map.get(key).size())
                    .forEach(k -> result.add(map.get(k).get(i))));
    
            // Display
            result.forEach(System.out::println);
        }
    }
    

    Output:

    Appliance [name=Fridge, price=100.0]
    Appliance [name=Oven, price=150.0]
    Appliance [name=DishWasher, price=220.0]
    Appliance [name=Fridge, price=200.0]
    Appliance [name=Oven, price=250.0]
    Appliance [name=DishWasher, price=320.0]
    Appliance [name=Fridge, price=300.0]
    Appliance [name=Oven, price=350.0]
    Appliance [name=DishWasher, price=420.0]
    

    [Update]

    Given below is the idiomatic code (Thanks to Holger) for the solution:

    List<Appliance> result = IntStream.range(0, map.values().stream().mapToInt(List::size).max().getAsInt())
                                .mapToObj(i -> map.values()
                                        .stream()
                                        .filter(list -> i < list.size())
                                        .map(list -> list.get(i)))
                                .flatMap(Function.identity()).collect(Collectors.toList());