2011-01-26 7 views
8

ジェネリックでタイプセーフなJava(map、filter、reduceなど)に共通の高次関数を書くことにしました。 1つの特定の機能。Javaジェネリックス - マップのような高次関数を実装する

だけで完了することが、ファンクタインタフェースはこれです:

/** 
* The interface containing the method used to map a sequence into another. 
* @param <S> The type of the elements in the source sequence. 
* @param <R> The type of the elements in the destination sequence. 
*/ 
public interface Transformation<S, R> { 

    /** 
    * The method that will be used in map. 
    * @param sourceObject An element from the source sequence. 
    * @return The element in the destination sequence. 
    */ 
    public R apply(S sourceObject); 
} 

厄介な機能がマップのようなものですが、代わりにコレクションを変換するには、最初に地図を(変換IそれがmapMapと呼ばれるべきだと思ったが、それは私がそれをremapEntriesと呼ぶことになったように、とても馬鹿に聞こえた)。

/** 
    * <p> 
    * Fills a map with the results of applying a mapping function to 
    * a source map. 
    * </p> 
    * Considerations: 
    * <ul> 
    * <li>The result map must be non-null, and it's the same object what is returned 
    * (to allow passing an unnamed new Map as argument).</li> 
    * <li>If the result map already contained some elements, those won't 
    * be cleared first.</li> 
    * <li>If various elements have the same key, only the last entry given the 
    * source iteration order will be present in the resulting map (it will 
    * overwrite the previous ones).</li> 
    * </ul> 
    * 
    * @param <SK> Type of the source keys. 
    * @param <SV> Type of the source values. 
    * @param <RK> Type of the result keys. 
    * @param <RV> Type of the result values. 
    * @param <MapRes> 
    * @param f The object that will be used to remapEntries. 
    * @param source The map with the source entries. 
    * @param result The map where the resulting entries will be put. 
    * @return the result map, containing the transformed entries. 
    */ 
    public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<Map.Entry<SK, SV>, Map.Entry<RK,RV>> f, final Map<SK, SV> source, MapRes result) { 
     for (Map.Entry<SK, SV> entry : source.entrySet()) { 
      Map.Entry<RK, RV> res = f.apply(entry); 
      result.put(res.getKey(), res.getValue()); 
     } 
     return result; 
    } 

そして、非常に正しいと思えるが、問題は、使用される変換が正確に一致しなければならないということです。

私の最初のバージョンは(と署名がかなりモンスターなので、SITを取る)でした互換性のある型のマップ関数を再利用するのが難しくなります。だから私は、署名にワイルドカードを追加することを決定し、そしてそれはこのように終わった:

public static <SK, SV, RK, RV, MapRes extends Map<RK, RV>> MapRes remapEntries(final Transformation<? super Map.Entry<? super SK, ? super SV>, ? extends Map.Entry<? extends RK, ? extends RV>> f, final Map<SK, SV> source, MapRes result) { 
    for (Map.Entry<SK, SV> entry : source.entrySet()) { 
     Map.Entry<? extends RK, ? extends RV> res = f.apply(entry); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 

しかし、私はそれをテストしようとしていたときに、ワイルドカードのマッチングは失敗します。

@Test 
public void testRemapEntries() { 
    Map<String, Integer> things = new HashMap<String, Integer>(); 
    things.put("1", 1); 
    things.put("2", 2); 
    things.put("3", 3); 

    Transformation<Map.Entry<String, Number>, Map.Entry<Integer, String>> swap = new Transformation<Entry<String, Number>, Entry<Integer, String>>() { 
     public Entry<Integer, String> apply(Entry<String, Number> sourceObject) { 
      return new Pair<Integer, String>(sourceObject.getValue().intValue(), sourceObject.getKey()); //this is just a default implementation of a Map.Entry 
     } 
    }; 

    Map<Integer, String> expected = new HashMap<Integer, String>(); 
    expected.put(1, "1"); 
    expected.put(2, "2"); 
    expected.put(3, "3"); 

    Map<Integer, String> result = IterUtil.remapEntries(swap, things, new HashMap<Integer, String>()); 
    assertEquals(expected, result); 
} 

エラーは次のとおりです。

method remapEntries in class IterUtil cannot be applied to given types 
    required: Transformation<? super java.util.Map.Entry<? super SK,? super SV>,? extends java.util.Map.Entry<? extends RK,? extends RV>>,java.util.Map<SK,SV>,MapRes 
    found: Transformation<java.util.Map.Entry<java.lang.String,java.lang.Number>,java.util.Map.Entry<java.lang.Integer,java.lang.String>>,java.util.Map<java.lang.String,java.lang.Integer>,java.util.HashMap<java.lang.Integer,java.lang.String> 

これを解決する方法に関するヒントはありますか?それとも、明示的なループをあきらめて書くべきでしょうか?^_^

+0

https://github.com/GlenKPetersonを見てみましょう/ fp4java7不変(または変更可能な)コレクションよりも遅延トランスフォームとして実装されたJavaの高次関数です。永続的な遅延変換も実装されています。これは完全な汎用インターフェースですが、実装にはいくつかのキャストが適切でした。 – GlenPeterson

+0

hehe、あなたは3年遅れ@GlenPeterson;)btw、いくつかのテストを追加! :D – fortran

答えて

5

Google Guava APIをご覧ください。

あなたのトランスフォームに似たFunctionインターフェイスがあります。マップインスタンスを作成または変換するユーティリティメソッドを持つクラスMapsもあります。

ジェネリックスの使用方法を実装する場合は、PECSも考慮する必要があります。

+0

私はGuavaが何をしているのか見てきましたが、地図のための別のTransformationクラスを持っています...それはあまりエレガントではありませんが、Java Genericsの限界まで行くことができる限りです。 – fortran

+1

+1グアバ。あなたはここに車輪を再発明しています。 Guavaには、あなたのニーズに合ったメソッドとクラスがあります。 –

+2

@Shervin私は、私がホイールを再発明していたことをすでに知っていましたが、ジェネリックセマンティクスにもっと慣れるための素晴らしいトレーニングです。 – fortran

5

これは難しい問題です。次の知識はまったく役に立たず、所有者に気を配るべきではありません。

最初に修正するのはタイプswapです。入力タイプはEntry<String,Number>であってはなりません。サブタイプではないEntry<String,Integer>を受け入れることができないからです。E<S,N>です。ただし、E<S,I>はサブタイプE<? extends S,? extends N>です。だから私たちの変圧器はそれを入力として取るべきです。出力のために、ワイアレスカードはありません。なぜなら、変圧器は、とにかく具体的なタイプのインスタンス化しかできないからです。私達はちょうど正直で消費することができるかを正確になりたいと何が生成されます。

/*  */ Transformation< 
        Entry<? extends String, ? extends Number>, 
        Entry<Integer, String> 
       > swap 
     = new Transformation< 
        Entry<? extends String, ? extends Number>, 
        Entry<Integer, String>>() 
    { 
     public Entry<Integer, String> apply(
      Entry<? extends String, ? extends Number> sourceObject) 
     { 
      return new Pair<Integer, String>(
       sourceObject.getValue().intValue(), 
       sourceObject.getKey() 
      ); 
     } 
    }; 

Stringが最終であると誰もがそれを拡張していないが、私は、一般的なシステムが知っているスマートではありません怖いですそれで、原則として、私は? extends Stringをやっていましたが、後でよかったです。

次に、remapEntries()について考えてみましょう。私たちは、それに渡すほとんどの変圧器が、swapと同様の型宣言を持つと考えています。したがって、その引数に適切に一致するようにすることをお勧めします。

remapEntry( 
    Transformation< 
     Entry<? extends SK, ? extends SV>, 
     Entry<RK,RV> 
     > f, 
    ... 

そこから、我々は、ソースと結果の型をうまく、我々は彼らができるだけ一般的になりたい:

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>> 
RM remapEntries(
    Transformation< 
     Entry<? extends SK, ? extends SV>, 
     Entry<RK,RV> 
     > f, 
    Map<? extends SK, ? extends SV> source, 
    RM result 
) 
{ 
    for(Entry<? extends SK, ? extends SV> entry : source.entrySet()) { 
     Entry<RK,RV> res = f.apply(entry); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 

RMは必要ありません、直接Map<? super RK, ? super RV>を使用するように罰金です。しかし、呼び出し元のコンテキストで戻り値の型がresultと同じになるようにしたいと思われます。返品のタイプをvoidにしました。すでに問題があります。

swap? extendsを使用しない場合、このことは失敗します。たとえば、入力タイプがString-Integerの場合、を入力するのはばかげています。しかし、このケースに合うように異なるパラメータ型宣言でオーバーロードメソッドを持つことができます。

わかりました。しかし、それは完全にではなく、という価値があります。あなたがそれを忘れてしまった場合、あなたの人生ははるかに良くなり、生の型を使用し、英語でパラメータを文書化し、実行時に型をチェックします。ジェネリック版は何でも買えますか?あなたのコードを完全にわかりにくくする巨大な価格で、ごくわずかです。私たちが明日の朝にメソッドシグネチャを読めば、あなた自身と自分自身を含む誰もそれを理解できません。それは正規表現よりはるかに悪いです。

+0

私は、ワイルドカードキャプチャ( '<?extends SK、?'f 'で使用されるSV>')と 'source'が一致しませんでした: -/ – fortran

+0

それは私にしばらくかかりましたが、今私は洞察力を持っていると思います! :-)キーは、キャプチャが外部タイプレベルでのみ行われることですが、ここでのワイルドカードは実際にはシグネチャの一部です。 – fortran

1

何かが突然私の頭の中に突入しました:入れ子にされたジェネリックパラメータのワイルドカードが文字通り型の一部としてキャプチャされない場合、マップでそれらを使用する代わりに逆の境界を使うことができますTransformation

public static <SK, SV, RK, RV, MapRes extends Map<? super RK, ? super RV>> 
    MapRes remapEntries(final Transformation<Map.Entry<SK, SV>, 
              Map.Entry<RK, RV>> f, 
         final Map<? extends SK, ? extends SV> source, 
         MapRes result) { 
    for (Map.Entry<? extends SK, ? extends SV> entry : source.entrySet()) { 
     Map.Entry<? extends RK, ? extends RV> res = f.apply((Map.Entry<SK, SV>)entry); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 

唯一の問題は、我々がTransformation.applyに未チェックのキャストをしなければならないということです。 Map.Entryのインターフェイスがの読み取り専用であれば、完全に安全です。そのため、変換がMap.Entry.setValueにコールしようとしていないことを願っています。

setValueメソッドが少なくともランタイム型の安全性を確保するために呼び出された場合、例外をスローしたMap.Entryインターフェイスの不変ラッパーを引き続き渡すことができます。

それとも明示的な不変のエントリインターフェイスを作成し、それを使用するが、それは(二つの異なる変換を持つものとして)不正行為のような少しです:

public interface ImmutableEntry<K, V> { 
    public K getKey(); 
    public V getValue(); 
} 

public static <SK, SV, RK, RV, RM extends Map<? super RK, ? super RV>> RM remapEntries(final Transformation<ImmutableEntry<SK, SV>, Map.Entry<RK, RV>> f, 
     final Map<? extends SK, ? extends SV> source, 
     RM result) { 
    for (final Map.Entry<? extends SK, ? extends SV> entry : source.entrySet()) { 
     Map.Entry<? extends RK, ? extends RV> res = f.apply(new ImmutableEntry<SK, SV>() { 
      public SK getKey() {return entry.getKey();} 
      public SV getValue() {return entry.getValue();} 
     }); 
     result.put(res.getKey(), res.getValue()); 
    } 
    return result; 
} 
関連する問題