4

I would like to map an NxN array into a Map in Java 8.

The idea is that every [i][0] element is a key and every [i][j] with j>0 is a list of values for every key in the map.

Thanks for any help. :)

This is my class:

public class GroupingDishes {

    public static void main(String[] args) {

        String[][] dishes = {
                {"Salad", "Tomato", "Cucumber", "Salad", "Sauce"},
                {"Pizza", "Tomato", "Sausage", "Sauce", "Dough"},
                {"Quesadilla", "Chicken", "Cheese", "Sauce"},
                {"Sandwich", "Salad", "Bread", "Tomato", "Cheese"}
        };

        Map<String, HashSet<String>> groupDishes = groupingDishes(dishes);
    }

    public static Map<String, HashSet<String>> groupingDishes(String[][] dishes) {

        Map<String, HashSet<String>> mapFood = new HashMap<>();

        for (int i = 0; i < dishes.length; i++) {

            String food = dishes[i][0];

            for (int j = 0; j < dishes[i].length; j++) {

                if (mapFood.containsKey(food)) {

                    HashSet<String> existingIngredients = mapFood.get(dishes[i][0]);
                    existingIngredients.add(dishes[i][j]);
                    mapFood.put(food, existingIngredients);

                } else {

                    HashSet<String> newIngredient = new HashSet<>();
                    mapFood.put(food, newIngredient);

                }
            }
        }
        return mapFood;
    }
}

2 Answers2

6

You could convert String[][] to a stream of String[], then collect to a map, using the first item of the String[] as the key, and the rest as the values of the set.

public static Map<String, HashSet<String>> groupingDishes2(String[][] dishes) {
    return Arrays.stream(dishes)
        .collect(Collectors.toMap(
            arr -> arr[0],
            arr -> Arrays.stream(arr).skip(1).collect(Collectors.toCollection(HashSet::new))));
}

Btw, I doubt you really need a Map<String, HashSet<String>>. It would be better to change the types to Map<String, Set<String>>, and then the implementation can be written simpler too.

public static Map<String, Set<String>> groupingDishes(String[][] dishes) {
    return Arrays.stream(dishes)
        .collect(Collectors.toMap(
            arr -> arr[0],
            arr -> Arrays.stream(arr).skip(1).collect(Collectors.toSet())));
}

Or even better, as @Holger suggested, an even better alternative, because "streams with skip and limit do not perform very well, also Collectors do not get any hint for the result's initial capacity":

public static Map<String, Set<String>> groupingDishes(String[][] dishes) {
    return Arrays.stream(dishes)
        .collect(Collectors.toMap(
            arr -> arr[0],
            arr -> new HashSet<>(Arrays.asList(arr).subList(1, arr.length))));
}
janos
  • 120,954
  • 29
  • 226
  • 236
  • 1
    I wouldn't use a stream operation for the second function, but rather `arr -> new HashSet<>(Arrays .asList(arr).subList(1, arr.length))` or, to create a list: `arr -> Arrays.asList(Arrays .copyOfRange(arr, 1, arr.length))`. – Holger Nov 22 '17 at 20:59
  • @Holger I'd like to know why a sub-list would be better. The second suggestion using an intermediary array cannot be better, because the array is unnecessary for creating a set. – janos Nov 22 '17 at 21:03
  • 4
    There are no intermediate arrays in my suggestions. The first suggestion creates a set from a *view* wrapping the original array. The second suggestion creates a list which *wraps* the new array. Either suggestion is shorter and more efficient than the stream operation. Streams with `skip` and `limit` do not perform very well, also `Collector`s do not get any hint for the result's initial capacity. – Holger Nov 22 '17 at 21:10
  • @Holger I'm gonna trust you on that and took your suggestion, thanks. But with all due respect, the second suggestion creates a new array, that's the unnecessary intermediary array I was referring to. – janos Nov 22 '17 at 21:26
  • 1
    @Eugene: `subList` only creates a view to the data, so it’s fast, but you have to take care when storing into a result, as it still refers the original data (an array longer than needed). That’s why I only used it when passing to `HashSet`’s constructor which will copy the data once. For the `List` variant, I used `Arrays.copyOfRange` as `Arrays.asList` does not copy. An alternative would be `new ArrayList<>(Arrays.asList(arr).subList(1, arr.length))`, but it’s longer… – Holger Nov 22 '17 at 21:26
  • 1
    @janos: the second suggestion is for the case the OP actually want a `List`; it creates a new array, which will be the very array, the final list will use as backing array (`Arrays.asList` does not copy). Just like the stream operation will return a new collection with a new backing array. I just used a simpler copying method. It’s possible to create lists without any copying, referencing the original arrays, but the result could exhibit surprising behavior, if the OP is not aware of it, so I didn’t suggest it. – Holger Nov 22 '17 at 21:30
  • 1
    By the way, you can avoid `skip` here by using `Arrays.stream(arr, 1, arr.length)`, still for a simple copy between arrays and/or collections, the Collections API is the idiomatic choice. – Holger Nov 22 '17 at 21:42
2

I can think of this:

Map<String, HashSet<String>> result = Arrays.stream(dishes)
            .map(x -> new AbstractMap.SimpleEntry<>(
                    x[0],
                    Arrays.stream(x).skip(1).collect(Collectors.toCollection(HashSet::new))))
            .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 4
    There is no need to store the key and value in an intermediate `SimpleEntry`, just to extract them in the subsequent `toMap` collector. – Holger Nov 22 '17 at 21:15