Java泛型 - 实现像map这样的高阶函数

时间:2011-01-26 10:49:15

标签: java generics functional-programming wildcard matching

我决定用Java编写一些常见的高阶函数(map,filter,reduce等),这些函数通过泛型是类型安全的,而且我遇到了在一个特定函数中匹配通配符的问题。

为了完成,函子界面是这样的:

/**
 * 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>

那么,有关如何解决此问题的任何提示?或者我应该放弃并为此编写显式循环? ^ _ ^

3 个答案:

答案 0 :(得分:5)

我认为你应该看看Google Guava API

在那里你可以找到一个与你的转换类似的Function界面。还有一个类Maps,其中包含用于创建或转换地图实例的实用程序方法。

在实施泛型使用方法时,您还应该考虑PECS

答案 1 :(得分: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,这件事就会失败。例如,如果输入类型为? extends,那么对它们{{1}}进行操作就太荒谬了。但是你可以使用不同参数类型声明的重载方法来匹配这种情况。

好的,出于好运,这很有效。但是,完全值得。如果你忘记它,你的生活会好得多,并使用原始类型,用英语记录参数,在运行时进行类型检查。问问自己,通用版本能为您买到什么吗?很少,以极高的价格渲染您的代码完全不可理解。如果我们明天早上阅读方法签名,那么包括你自己和我在内的任何人都无法理解它。它比正则表达式要糟糕得多。

答案 2 :(得分:1)

突然出现了一些东西:如果嵌套通用参数中的通配符不会被捕获,因为它们实际上是该类型的一部分,那么我可以在地图中使用反向边界而不是在{ {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方法以确保至少运行时类型安全,我们仍然可以传递Map.Entry接口的不可变包装器,该接口会引发异常。

或者只是创建一个显式的不可变Entry接口并使用它,但这有点像作弊(因为有两个不同的转换):

setValue