Java 8 Stream Collectors - Collector to create a Map with objects in multiple buckets

人盡茶涼 提交于 2020-12-05 20:23:09

问题


The following code works and is readable but it seems to me I have intermediate operations that feel like they shouldn't be necessary. I've written this simplified version as the actual code is part of a much larger process.

I've got a Collection of Widget, each with a name and multiple types (indicated by constants of the WidgetType enum). These multiple types are gettable as a Stream<WidgetType> though, if necessary, I could return those as some other type. (For various reasons, it is strongly desirable that these be returned as a Stream<WidgetType> because of how these widgets are used later in the actual code.)

These widgets are added to an EnumMap<WidgetType, List<Widget>> which is, later, translated into an EnumMap<WidgetType, Widget[]>.

If each Widget only had a single WidgetType, this would be a trivial solve but, since any Widget could have 1 or more types, I am tripping all over myself with the syntax of the Collectors.groupingBy() method (and its overloads).

Here's the code example, again, fully functional and gives me the exact result I need.

class StackOverFlowExample {

    private final Map<WidgetType, Widget[]> widgetMap = new EnumMap<>(WidgetType.class);

    public static void main(String[] args) { new StackOverFlowExample(); }

    StackOverFlowExample() {
        Collection<Widget> widgetList = getWidgetsFromWhereverWidgetsComeFrom();

        {
            final Map<WidgetType, List<Widget>> intermediateMap = new EnumMap<>(WidgetType.class);
            widgetList.forEach(w ->
                    w.getWidgetTypes().forEach(wt -> {
                        intermediateMap.putIfAbsent(wt, new ArrayList<>());
                        intermediateMap.get(wt).add(w);
                    })
            );
            intermediateMap.entrySet().forEach(e -> widgetMap.put(e.getKey(), e.getValue().toArray(new Widget[0])));
        }

        Arrays.stream(WidgetType.values()).forEach(wt -> System.out.println(wt + ": " + Arrays.toString(widgetMap.get(wt))));
    }

    private Collection<Widget> getWidgetsFromWhereverWidgetsComeFrom() {
        return Arrays.asList(
                new Widget("1st", WidgetType.TYPE_A, WidgetType.TYPE_B),
                new Widget("2nd", WidgetType.TYPE_A, WidgetType.TYPE_C),
                new Widget("3rd", WidgetType.TYPE_A, WidgetType.TYPE_D),
                new Widget("4th", WidgetType.TYPE_C, WidgetType.TYPE_D)
        );
    }

}

This outputs:

TYPE_A: [1st, 2nd, 3rd]
TYPE_B: [1st]
TYPE_C: [2nd, 4th]
TYPE_D: [3rd, 4th]

For completeness sake, here's the Widget class and the WidgetType enum:

class Widget {

    private final String       name;
    private final WidgetType[] widgetTypes;

    Widget(String n, WidgetType ... wt) { name = n; widgetTypes = wt; }

    public String getName() { return name; }
    public Stream<WidgetType> getWidgetTypes() { return Arrays.stream(widgetTypes).distinct(); }

    @Override public String toString() { return name; }
}

enum WidgetType { TYPE_A, TYPE_B, TYPE_C, TYPE_D }

Any ideas on a better way to execute this logic are welcome. Thanks!


回答1:


IMHO, the key is to convert a Widget instance to a Stream<Pair<WidgetType, Widget>> instance. Once we have that, we can flatMap a stream of widgets and collect on the resulting stream. Of course we don't have Pair in Java, so have to use AbstractMap.SimpleEntry instead.

widgets.stream()
       // Convert a stream of widgets to a stream of (type, widget)
       .flatMap(w -> w.getTypes().map(t->new AbstractMap.SimpleEntry<>(t, w)))
       // Grouping by the key, and do additional mapping to get the widget
       .collect(groupingBy(e->e.getKey(),
                mapping(e->e.getValue, 
                        collectingAndThen(toList(), l->l.toArray(new Widget[0])))));

P.S. this is an occasion where IntelliJ's suggestion doesn't shorten a lambda with method reference.




回答2:


This is a bit convoluted, but it produces the same output, not necessarily in the same order. It uses a static import of java.util.stream.Collectors.*.

widgetMap = widgetList.stream()
        .flatMap(w -> w.getWidgetTypes().map(t -> new AbstractMap.SimpleEntry<>(t, w)))
        .collect(groupingBy(Map.Entry::getKey, collectingAndThen(mapping(Map.Entry::getValue, toSet()), s -> s.stream().toArray(Widget[]::new))));

Output on my machine:

TYPE_A: [1st, 3rd, 2nd]
TYPE_B: [1st]
TYPE_C: [2nd, 4th]
TYPE_D: [3rd, 4th]


来源:https://stackoverflow.com/questions/41668001/java-8-stream-collectors-collector-to-create-a-map-with-objects-in-multiple-bu

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!