Como construir um mapa que replica uma função na API Lambda de Java
-
21-12-2019 - |
Pergunta
A partir de um java.util.function.BiFunction
que mapeia um par de Enum
é um valor, quero construir um EnumMap
que reflete esse mapeamento.
Por exemplo, deixe E1
e E2
ser enum
tipos e T
qualquer tipo:
BiFunction<E1,E2, T> theBiFunction = //...anything
EnumMap<E1,EnumMap<E2,T>> theMap =
buildTheMap( // <-- this is where the magic happens
E1.values(),
E2.values(),
theBiFunction);
Dado qualquer par de valores do tipo E1
e E2
E1 e1 = //any valid value...
E2 e2 = //any valid value....
ambos os valores abaixo devem ser iguais:
T valueFromTheMaps = theMap.get(e1).get(e2);
T valueFromTheFunction = theBiFunction.apply(e1,e2);
boolean alwaysTrue = valueFromTheMaps.equals(valueFromTheFunction);
Qual é a melhor implementação (mais elegante, eficiente, etc...) para o método onde o "Magia" acontece em?
Solução
Você obtém uma solução elegante se optar por uma solução genérica e decompô-la.Primeiro, implemente uma função genérica que crie um EnumMap
fora de um Function
, então implemente o mapeamento aninhado de um BiFunction
usando a primeira função combinada consigo mesma:
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);
}
Aqui está um pequeno caso de teste:
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}}
Claro, usando a solução genérica você pode construir métodos para concreto enum
tipos que não exigem Class
parâmetro(s)…
Isso deve funcionar sem problemas com um fluxo paralelo se o fornecido (Bi)Function
é thread-safe.
Outras dicas
Como base de comparação, aqui está a versão convencional:
<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;
}
Agora, aqui está uma versão que usa formas aninhadas de três argumentos do collect()
operação do terminal de fluxo:
<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);
}
O que torna isso complicado é que a função acumuladora do coletor externo precisa executar um fluxo com seu próprio coletor de três argumentos para produzir o mapa interno.Isso é realmente difícil de recuar bem.Em vez do espaçamento padrão, alinhei os três argumentos para cada collect()
chamar.Isso o torna bastante amplo, mas se eu não fizesse isso, seria difícil ver o que acontece, já que o aninhamento é muito profundo.Por mais fã de streams que eu seja, é difícil para mim dizer que esta é melhor do que a versão convencional.
Você pode dizer: "Por que não usar toMap()
em vez do três argumentos collect()
função?" O problema é que precisamos criar EnumMap
casos, e a sobrecarga de toMap()
que leva um fornecedor de mapas tem quatro argumentos:
toMap(keyFunc, valueFunc, mergeFunc, mapSupplier)
Pior ainda, a função de mesclagem (terceiro argumento) não é usada, então teríamos que fornecer uma função que nunca é usada.Aqui está o que parece:
<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)));
}
Não me parece melhor.Meu dinheiro ainda está na versão convencional.
Existem várias abordagens alternativas que podemos tentar.Veremos o que uma boa noite de sono traz.