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

624 Views Asked by At

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);
        }
    }
}
3

There are 3 best solutions below

5
On BEST ANSWER

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

If you do not mind the order Fridge -> Oven -> DishWasher,the below code is helpful:

map.values().stream().flatMap((Function<List<Appliance>, Stream<Appliance>>) Collection::stream)
    .collect(Collectors.groupingBy(appliance -> {
    List<Appliance> appliances = map.get(appliance.getName());
    for (int i = 0;i<appliances.size();i++) {
        if (appliance.getPrice() == appliances.get(i).getPrice()) {
            return i;
        }
    }
    return 0;
})).values().stream().flatMap((Function<List<Appliance>, Stream<Appliance>>) Collection::stream)
    .forEach(System.out::println);
6
On
map.keySet().stream().map(map::get).forEach(list::addAll);

.addAll() in a .stream() can do the job.

Now that you have all the elements in the list, you can sort it:

list.sort(Comparator.comparing(Object::toString)
    .thenComparingInt(s -> Integer.parseInt(
            s.toString()
             .substring(0, s.toString().length() - 1)
             .split(",")[1].trim())));