web-dev-qa-db-fra.com

Impossible d'utiliser Java 8 avec des arguments lambda sans spécifier d'arguments de type

J'ai créé une méthode avec des arguments de type, en renvoyant un type générique en utilisant ces arguments de type et en prenant des arguments Function qui dépendent également des arguments de type. Lorsque j'utilise des lambdas comme arguments, le compilateur m'oblige à spécifier les arguments de type de la méthode, ce qui ne va pas.

Je conçois une classe utilitaire avec des méthodes à utiliser avec Stream.flatMap. Il mappe chaque type d'entrée de collection à un FlatEntry qui contient un élément clé et valeur, et peut le faire à plusieurs niveaux avec un générateur. La méthode affectée est flatEntryMapperBuilder. Voici le code:

import Java.util.function.Function;
import Java.util.stream.Stream;

public class GdkStreams
{
    public static <T, K, V> Function<T, Stream<FlatEntry<K, V>>> flatEntryMapper(Function<T, K> keyMapper,
                                                                                 Function<T, Stream<V>> valueMapper)
    {
        return input -> {
            K key = keyMapper.apply(input);
            return valueMapper.apply(input).map(value -> new FlatEntry<>(key, value));
        };
    }

    public static <T, K, V> FlatEntryMapperBuilder<T, K, V> flatEntryMapperBuilder(Function<T, K> keyMapper,
                                                                                   Function<T, Stream<V>> valueMapper)
    {
        return new FlatEntryMapperBuilder<>(keyMapper, valueMapper);
    }

    public static class FlatEntryMapperBuilder<T, K, V>
    {
        private Function<T, K>         keyMapper;

        private Function<T, Stream<V>> valueMapper;

        private FlatEntryMapperBuilder (Function<T, K> keyMapper, Function<T, Stream<V>> valueMapper)
        {
            this.keyMapper = keyMapper;
            this.valueMapper = valueMapper;
        }

        public Function<T, Stream<FlatEntry<K, V>>> build()
        {
            return flatEntryMapper(keyMapper, valueMapper);
        }

        public <K2, V2> FlatEntryMapperBuilder<T, K, FlatEntry<K2, V2>> chain(Function<V, K2> keyMapper2,
                                                                              Function<V, Stream<V2>> valueMapper2)
        {
            return new FlatEntryMapperBuilder<>(keyMapper,
                                                valueMapper.andThen(stream -> stream.flatMap(flatEntryMapper(keyMapper2,
                                                                                                             valueMapper2))));
        }
    }

    public static class FlatEntry<K, V>
    {
        public final K key;

        public final V value;

        public FlatEntry (K key, V value)
        {
            this.key = key;
            this.value = value;
        }
    }
}

Le problème vient de son utilisation. Dis que j'ai:

Map<String, Set<String>> level1Map;

Je peux mapper chaque élément des sous-ensembles à un FlatEntry en faisant:

level1Map.entrySet().stream().flatMap(GdkStreams.flatEntryMapper(Entry::getKey, entry -> entry.getValue().stream()));

Et cela fonctionne très bien. Mais quand j'essaye de faire ça:

level1Map.entrySet()
         .stream()
         .flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build());

Le compilateur Eclipse (Mars 4.5.0) rompt avec:

- The type Map.Entry does not define getKey(Object) that is applicable here
- The method getValue() is undefined for the type Object
- Type mismatch: cannot convert from GdkStreams.FlatEntryMapperBuilder<Object,Object,Object> to 
 <unknown>

Et javac (1.8.0_51) rompt avec:

MainTest.Java:50: error: incompatible types: cannot infer type-variable(s) T,K#1,V#1
                 .flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build());
                                                           ^
    (argument mismatch; invalid method reference
      method getKey in interface Entry<K#2,V#2> cannot be applied to given types
        required: no arguments
        found: Object
        reason: actual and formal argument lists differ in length)
  where T,K#1,V#1,K#2,V#2 are type-variables:
    T extends Object declared in method <T,K#1,V#1>flatEntryMapperBuilder(Function<T,K#1>,Function<T,Stream<V#1>>)
    K#1 extends Object declared in method <T,K#1,V#1>flatEntryMapperBuilder(Function<T,K#1>,Function<T,Stream<V#1>>)
    V#1 extends Object declared in method <T,K#1,V#1>flatEntryMapperBuilder(Function<T,K#1>,Function<T,Stream<V#1>>)
    K#2 extends Object declared in interface Entry
    V#2 extends Object declared in interface Entry
MainTest.Java:50: error: invalid method reference
                 .flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build());
                                                            ^
  non-static method getKey() cannot be referenced from a static context
  where K is a type-variable:
    K extends Object declared in interface Entry
2 errors

Si je remplace Entry::getKey Par entry -> entry.getKey(), javac change radicalement sa sortie:

MainTest.Java:51: error: cannot find symbol
                 .flatMap(GdkStreams.flatEntryMapperBuilder(entry -> entry.getKey(), entry -> entry.getValue().stream()).build());

                                                                          ^
  symbol:   method getKey()
  location: variable entry of type Object
MainTest.Java:51: error: cannot find symbol
                 .flatMap(GdkStreams.flatEntryMapperBuilder(entry -> entry.getKey(), entry -> entry.getValue().stream()).build());

                                                                                                   ^
  symbol:   method getValue()
  location: variable entry of type Object
2 errors

Il compile bien en spécifiant les paramètres de type, ce que j'attendais:

level1Map.entrySet()
         .stream()
         .flatMap(GdkStreams.<Entry<String, Set<String>>, String, String> flatEntryMapperBuilder(Entry::getKey,
                                                                                                 entry -> entry.getValue()
                                                                                                               .stream())
                            .build());

ou en spécifiant l'un des paramètres de type d'argument:

Function<Entry<String, Set<String>>, String> keyGetter = Entry::getKey;
level1Map.entrySet()
         .stream()
         .flatMap(GdkStreams.flatEntryMapperBuilder(keyGetter, entry -> entry.getValue().stream()).build());

Mais c'est maladroit! Imaginez maintenant à quel point il serait maladroit d'écrire tous les paramètres de type avec 2 niveaux dans la carte, en utilisant la méthode de chaîne (qui est mon utilisation cible):

Map<String, Map<String, Set<String>>> level2Map;

J'ai lu beaucoup d'autres questions sur l'inférence de type lambdas et générique, mais aucune ne répond à mon cas particulier.

Suis-je en train de manquer quelque chose? Puis-je corriger mon API pour que son utilisation soit moins maladroite, ou suis-je obligé de toujours spécifier des arguments de type? Merci!

14
David Dersigny

Holger a eu la meilleure réponse dans la section des commentaires à mon avis:

Il s'agit d'une limitation connue de Java 8 inférence de type: elle ne fonctionne pas avec les invocations de méthodes chaînées comme genericFactoryMethod().build().

Merci! À propos de mon API, je spécifierai les fonctions avant de les utiliser comme arguments, comme ceci:

Function<Entry<String, Set<String>>, String> keyMapper = Entry::getKey;
Function<Entry<String, Set<String>>, Stream<String>> valueMapper = entry -> entry.getValue().stream();

EDIT: J'ai repensé l'API grâce aux commentaires de Holger (merci encore!). Il conserve l'élément d'origine au lieu d'une clé, ainsi que la valeur aplatie.

public static <T, R> Function<? super T, Stream<FlatEntry<T, R>>> flatEntryMapper(Function<? super T, ? extends Stream<? extends R>> mapper)
{
    return element -> mapper.apply(element).map(value -> new FlatEntry<>(element, value));
}

public static class FlatEntry<E, V>
{
    /** The original stream element */
    public final E element;

    /** The flattened value */
    public final V value;

    private FlatEntry (E element, V value)
    {
        this.element = element;
        this.value = value;
    }
}

Il est chaînable, à partir du niveau 2, le mappeur doit traiter un FlatEntry. L'utilisation est similaire à un simple flatMap:

Map<String, Map<String, Map<String, Set<String>>>> level3Map;

// gives a stream of all the flattened values
level3Map.entrySet()
         .stream()
         .flatMap(entry -> entry.getValue().entrySet().stream())
         .flatMap(entry -> entry.getValue().entrySet().stream())
         .flatMap(entry -> entry.getValue().stream());

// gives a stream of FlatEntries with flattened values and all their original elements in nested FlatEntries
level3Map.entrySet()
         .stream()
         .flatMap(GdkStreams.flatEntryMapper(entry -> entry.getValue().entrySet().stream()))
         .flatMap(GdkStreams.flatEntryMapper(flatEntry -> flatEntry.value.getValue().entrySet().stream()))
         .flatMap(GdkStreams.flatEntryMapper(flatEntry -> flatEntry.value.getValue().stream()));
10
David Dersigny

Une façon de fournir suffisamment d'informations de type au compilateur consiste à déclarer un type explicite de l'un des arguments lambda. C'est dans le même esprit que votre réponse mais un peu plus compact, puisque vous n'avez qu'à fournir le type de l'argument, pas la fonction entière.

Cela semble assez bien pour la carte à un niveau:

level1Map.entrySet().stream()
    .flatMap(GdkStreams.flatEntryMapperBuilder(
        (Entry<String, Set<String>> entry) -> entry.getKey(), 
        entry -> entry.getValue().stream()).build());

La carte à deux niveaux est à la frontière du grotesque, cependant:

level2Map.entrySet().stream()
    .flatMap(GdkStreams.flatEntryMapperBuilder(
        (Entry<String, Map<String, Set<String>>> entry1) -> entry1.getKey(), 
        entry1 -> entry1.getValue().entrySet().stream()
            .flatMap(GdkStreams.flatEntryMapperBuilder(
                (Entry<String, Set<String>> entry2) -> entry2.getKey(), 
                entry2 -> entry2.getValue().stream()).build())).build());
4
Lii