1

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
    You have created 5 questions today about doing various things with streams. Others have been okay, but this one doesn't show an attempt. Stack Overflow should be your last resource, not your first. We expect you to have given the problem a decent go yourself, and given the frequency with which you're asking, I'm not sure you are. – Michael May 14 '20 at 22:12
  • @Michael, I am studying the Stream API, and I am asking questions as my study progresses and I think of various ways that I could use the API. I apologise for using SO in a way different than expected, I will not post more similar questions. –  May 14 '20 at 22:20
  • 1
    You were right to point out the weak status of this question. I have added the itterrative implementation of my question to the post. –  May 15 '20 at 00:20

3 Answers3

1

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());
Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
  • 1
    When iterating or stream over a map, you should select the view matching what you are interested in. You are interested in the *values* of the map so the natural choice should be the `values()` view, rather than using `keySet()` and performing redundant `get(key)` calls on the `map`. In other words, instead of `map.keySet() .stream() .filter(key -> i < map.get(key).size()) .forEach(k -> result.add(map.get(k).get(i))));` you can use the much simpler `map.values() .stream() .filter(list -> i < list.size()) .forEach(list -> result.add(list.get(i)));` – Holger May 15 '20 at 10:52
  • 1
    Then, replace the `List result = new ArrayList<>(); … .forEach(… .forEach(… result.add(…))` with the idiomatic `List result = … .mapToObj(… .map(…)) .collect(Collectors.toList());` – Holger May 15 '20 at 10:54
  • Thanks, @Holger for taking the time to review my answer. Feedbacks from you and other experts have been continuously helping me improve the quality of my answers. I was able to understand the first comment completely but I've not updated the answer to incorporate your comments because I'm struggling to implement your second comment (though I can understand the concept). I need further help from you. – Arvind Kumar Avinash May 15 '20 at 14:36
  • I suppose, the obstacle was that a `flatMap` is needed: `List result = IntStream.range(0, map.values().stream().mapToInt(List::size).max().orElse(0)) .mapToObj(i -> map.values().stream() .filter(list -> i < list.size()).map(list -> list.get(i))) .flatMap(Function.identity()) .collect(Collectors.toList());` – Holger May 15 '20 at 15:57
  • Thanks, @Holger for the response. Now, it is clear to me. I've also incorporated this into the answer as an update so that future visitors do not miss this valuable piece of code. – Arvind Kumar Avinash May 15 '20 at 16:08
0
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())));
Harshal Parekh
  • 5,918
  • 4
  • 21
  • 43
  • 1
    thank you! This does not answer the questiont though, as it does not add the elements in the list in the order described in the question. –  May 14 '20 at 22:57
  • Can you explain the comparator purpose? – Hadi J May 15 '20 at 01:31
  • Comparator is used for custom sorting. In this case, we need to sort on the integer value inside the string; it’s not default behaviour, comparator tells what to sort on and how to sort. You might want to read the java doc, it is a very important concept. – Harshal Parekh May 15 '20 at 01:34
  • 2
    Your custom comparator is really so strange. I reckon you've supposed that the first element of 'Fridge' is always less than 'Oven' and 'DishWasher' and even for the second and third elements. my question is how can you guarantee this? In addition, if this assumption is valid your comparator should be like this: `list.sort(Comparator.comparing(Appliance::getPrice));` – Hadi J May 15 '20 at 01:58
  • @HadiJ, looks like I forgot about the first case. Updated the comparator. – Harshal Parekh May 15 '20 at 02:23
  • Could you please explain this part of the solution? .thenComparingInt(s -> Integer.parseInt( s.toString() .substring(0, s.toString().length() - 1) .split(",")[1].trim()))); –  May 15 '20 at 10:50
0

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);
TongChen
  • 1,414
  • 1
  • 11
  • 21