How to build a Map that replicates a Function in Java's Lambda API

梦想与她 提交于 2020-01-02 07:01:12

问题


From a java.util.function.BiFunction that maps a pair of Enums into a value, I want to build a EnumMap that reflects that mapping.

For instance, let E1 and E2 be enum types and T any given type:

 BiFunction<E1,E2, T> theBiFunction = //...anything

 EnumMap<E1,EnumMap<E2,T>> theMap = 
    buildTheMap(                     // <--  this is where the magic happens
                E1.values(), 
                E2.values(),
                theBiFunction);

Given any pair of values of type E1 and E2

E1 e1 = //any valid value...
E2 e2 = //any valid value....

both values below should be equal:

T valueFromTheMaps = theMap.get(e1).get(e2);
T valueFromTheFunction = theBiFunction.apply(e1,e2);

boolean alwaysTrue = valueFromTheMaps.equals(valueFromTheFunction);

What's the best (more elegant, efficient, etc...) implementation for the method where the "magic" takes place?


回答1:


You get an elegant solution if you go to a generic solution and break it down. First, implement a generic function which creates an EnumMap out of a Function, then implement the nested mapping of a BiFunction using the first function combined with itself:

static <T,E extends Enum<E>>
  EnumMap<E,T> funcToMap(Function<E,T> f, Class<E> t, E... values) {
    return Stream.of(values)
      .collect(Collectors.toMap(Function.identity(), f, (x,y)->x, ()-> new EnumMap<>(t)));
}
static <T,E1 extends Enum<E1>,E2 extends Enum<E2>>
  EnumMap<E1,EnumMap<E2,T>> biFuncToMap(
  BiFunction<E1,E2,T> f, Class<E1> t1, Class<E2> t2, E1[] values1, E2[] values2){

  return funcToMap(e1->funcToMap(e2->f.apply(e1, e2), t2, values2), t1, values1);
}

Here’s a little test case:

enum Fruit {
    APPLE, PEAR
}
enum Color {
    RED, GREED, YELLOW
}

EnumMap<Fruit, EnumMap<Color, String>> result
  =biFuncToMap((a,b)->b+" "+a,
     Fruit.class, Color.class, Fruit.values(), Color.values());
System.out.println(result);

{APPLE={RED=RED APPLE, GREED=GREED APPLE, YELLOW=YELLOW APPLE}, PEAR={RED=RED PEAR, GREED=GREED PEAR, YELLOW=YELLOW PEAR}}

Of course, using the generic solution you can built methods for concrete enum types which do not require the Class parameter(s)…


This ought to work smoothly with a parallel stream if the provided (Bi)Function is thread safe.




回答2:


As a baseline for comparison, here's the conventional version:

<T> EnumMap<E1,EnumMap<E2,T>> buildTheMap(E1[] e1values,
                                          E2[] e2values,
                                          BiFunction<E1,E2,T> f) {
    EnumMap<E1,EnumMap<E2,T>> outer = new EnumMap<>(E1.class);
    for (E1 e1 : e1values) {
        EnumMap<E2,T> inner = new EnumMap<>(E2.class);
        for (E2 e2 : e2values) {
            inner.put(e2, f.apply(e1, e2));
        }
        outer.put(e1, inner);
    }
    return outer;
}

Now here's a version that uses nested three-arg forms of the collect() stream terminal operation:

<T> EnumMap<E1,EnumMap<E2,T>> buildTheMap(E1[] e1values,
                                          E2[] e2values,
                                          BiFunction<E1,E2,T> f) {
    return
        Stream.of(e1values)
              .collect(() -> new EnumMap<>(E1.class),
                       (map, e1) -> map.put(e1, Stream.of(e2values)
                                                      .collect(() -> new EnumMap<>(E2.class),
                                                               (m, e2) -> m.put(e2, f.apply(e1, e2)),
                                                               Map::putAll)),
                       Map::putAll);
}

What makes this cumbersome is that the accumulator function for the outer collector has to run a stream with its own three-arg collector to produce the inner map. This is really hard to indent well. Instead of standard spacing, I've lined up the three arguments to each collect() call. This makes it pretty wide, but if I didn't do this, it would be hard to see what goes with what since the nesting is so deep. As much of a fan of streams as I am, it's hard for me to say that this is any better than the conventional version.

You might say, "Why not use toMap() instead of the three-arg collect() function?" The problem is that we need to create EnumMap instances, and the overload of toMap() that takes a map supplier has four arguments:

toMap(keyFunc, valueFunc, mergeFunc, mapSupplier)

Worse, the merge function (third arg) isn't used, so we'd have to supply a function that's never used. Here's what that looks like:

<T> EnumMap<E1,EnumMap<E2,T>> buildTheMap(E1[] e1values,
                                          E2[] e2values,
                                          BiFunction<E1,E2,T> f) {
    return
        Stream.of(e1values)
              .collect(toMap(e1 -> e1,
                             e1 -> Stream.of(e2values)
                                         .collect(toMap(e2 -> e2,
                                                        e2 -> f.apply(e1, e2),
                                                        (x, y) -> x,
                                                        () -> new EnumMap<>(E2.class))),
                             (x, y) -> x,
                             () -> new EnumMap<>(E1.class)));
}

Doesn't look any better to me. My money is still on the conventional version.

There are a number of alternative approaches one could try. We'll see what a good night's sleep brings.



来源:https://stackoverflow.com/questions/25049107/how-to-build-a-map-that-replicates-a-function-in-javas-lambda-api

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