web-dev-qa-db-ja.com

型引数を指定せずにJava 8メソッドとラムダ引数を使用することはできません

型引数を使用してメソッドを作成し、これらの型引数を使用してジェネリック型を返し、型引数にも依存するFunction引数を取得しました。引数としてラムダを使用すると、コンパイラーはメソッドの型引数を指定するように強制しますが、これは間違っていると感じます。

_Stream.flatMap_で使用するメソッドを含むユーティリティクラスを設計しています。すべての種類のコレクションエントリを、キーと値の要素を含むFlatEntryにマップし、ビルダーを使用して複数のレベルでこれを行うことができます。影響を受けるメソッドはflatEntryMapperBuilderです。これがコードです:

_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;
        }
    }
}
_

問題はその使い方です。私が持っていると言います:

_Map<String, Set<String>> level1Map;
_

次のようにして、sub Setsのすべての要素をFlatEntryにマップできます。

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

そして、それはうまくいきます。しかし、私がこれをやろうとすると:

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

Eclipse(Mars 4.5.0)コンパイラは、次のように動作します。

_- 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>
_

そしてjavac(1.8.0_51)は次のように壊れます:

_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
_

_Entry::getKey_をentry -> entry.getKey()に置き換えると、javacは出力を大幅に変更します。

_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
_

タイプパラメータを指定することで問題なくコンパイルできます。

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

または、引数型パラメーターの1つを指定します。

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

しかし、これは不器用です!チェーンメソッドを使用して、マップに2つのレベルのすべての型パラメーターを書き込むのがどれほど不格好であるかを想像してみてください(これはターゲットの使用法です)。

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

ラムダとジェネリックの型推論に関する他の多くの質問を読みましたが、私の特定のケースに答えているものはありません。

何か不足していますか? APIを修正して、使用法が不便にならないようにできますか、それとも常に型引数を指定するのに悩まされていますか?ありがとう!

14
David Dersigny

Holgerは、私の意見のコメントセクションで最良の回答を示しました。

これは、Java 8の型推論の既知の制限です。genericFactoryMethod().build()のようなチェーンメソッド呼び出しでは機能しません。

ありがとう!私のAPIについては、次のように、関数を引数として使用する前に指定します。

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

編集:私はHolgerのコメントのおかげでAPIを再設計しました(ありがとうございます!)。平坦化された値とともに、キーではなく元の要素を保持します。

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;
    }
}

これは連鎖可能であり、レベル2以降、マッパーはFlatEntryを処理する必要があります。使い方は単純な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

コンパイラに十分な型情報を提供する1つの方法は、ラムダ引数の1つの明示的な型を宣言することです。これは your answer と同じ趣旨ですが、関数全体ではなく、引数のタイプのみを指定する必要があるため、もう少しコンパクトになります。

これは、1レベルのマップにはかなり問題ありません。

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

ただし、2レベルのマップはグロテスクの境界にあります。

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