2011-01-26 66 views
8

我決定在Java中編寫一些通用的高階函數(地圖,過濾器,縮小等),這些類型是通過泛型類型安全的,而且我遇到了通配符匹配問題一個特定的功能。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); 
} 

令人不安的功能就像是一個地圖,但不是轉化集合它把一個地圖(起初我認爲它應該被稱爲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; 
    } 

而且這似乎是相當正確的,但問題是,所用的轉換必須完全匹配類型參數,使得兼容的類型難以重用map函數。所以我決定通配符添加到簽名,並且它弄成這個樣子:

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 2014-02-19 16:50:38

+0

呵呵,你遲了3年@GlenPeterson;)btw,加一些測試! :D – fortran 2014-02-20 00:51:30

回答

5

我想你應該看看Google Guava API

在那裏你可以找到一個Function接口類似於你的轉換之一。還有一個帶有實用方法的類Maps來創建或轉換映射實例。

在實現泛型使用方法時,您還應該考慮PECS

+0

我已經看過Guava做了什麼,他們有一個單獨的Transformation類,用於映射......它不是很優雅,但我想這就像你可以用Java泛型限制一樣。 – fortran 2011-01-26 12:06:37

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類型相同。我woulda只是作出了返回類型void - 已經有足夠的麻煩。

如果swap不使用? extends,則此事將失敗。例如,如果輸入類型是String-Integer,那麼執行? extends就太荒謬了。但是您可以使用不同的參數類型聲明的重載方法來匹配這種情況。

好的,這工作,出於純粹的運氣。但是,它完全是不是值得。如果你忘了它,你的生活會更好,並且使用原始類型,用英語記錄參數,在運行時進行類型檢查。問問自己,通用版本是否會爲你購買任何東西?很少,以極大的代價完全不能理解你的代碼。如果我們明天早上讀了方法簽名,沒有人,包括你自己和我自己,都可以理解它。它比正則表達式差得多。

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