【问题标题】:Java generics - implementing higher order functions like mapJava 泛型 - 实现高阶函数,如 map
【发布时间】:2011-06-15 18:36:42
【问题描述】:

我决定用 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);
}

麻烦的函数就像一个map,但不是转换一个Collection,而是转换一个Map(一开始我以为应该是叫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;
    }

而且看起来很正确,但问题是所使用的转换必须与类型参数完全匹配,从而难以将映射函数重用于兼容的类型。所以我决定在签名中添加通配符,结果是这样的:

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>

那么,关于如何解决这个问题的任何提示?还是我应该放弃并为此编写显式循环? ^_^

【问题讨论】:

  • 看看github.com/GlenKPeterson/fp4java7 它是Java 的高阶函数,实现为对不可变(或可变)集合的惰性转换。还实现了一些持久的惰性转换。它是一个完全通用的接口,尽管在实现中进行了一些适当的转换。
  • 呵呵,你迟到了 3 年 @GlenPeterson ;) 顺便说一句,添加一些测试! :D

标签: java generics functional-programming wildcard matching


【解决方案1】:

我突然想到了一些事情:如果嵌套泛型参数中的通配符不会被捕获,因为它们实际上是类型的一部分,那么我可以在映射中使用反向边界,而不是在 @ 中使用它们987654321@.

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 接口的不可变包装器以引发异常。

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

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

【讨论】:

    【解决方案2】:

    这是一个困难的问题。以下知识完全没用,任何人都不应关心拥有:

    首先要修复的是swap 的类型。输入类型不应该是Entry&lt;String,Number&gt;,因为那样它就不能接受Entry&lt;String,Integer&gt;,它不是E&lt;S,N&gt; 的子类型。但是,E&lt;S,I&gt; 是E&lt;? extends S,? extends N&gt; 的子类型。所以我们的变压器应该把它作为输入。对于输出,没有通配符,因为转换器无论如何只能实例化一个具体类型。我们只想诚实准确地说明可以消费什么以及生产什么:

        /*     */ 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&lt;? super RK, ? super RV&gt;就可以了。但您似乎希望返回类型与调用者上下文中的 result 类型相同。我会简单地将返回类型设置为void - 麻烦已经够多了。

    如果swap 不使用? extends,这件事就会失败。例如,如果输入类型是String-Integer,那么在其中做? extends 是很可笑的。但是您可以使用具有不同参数类型声明的重载方法来匹配这种情况。

    好的,这完全是幸运的。但是,这完全不值得。如果你忘记它,使用原始类型,用英文记录参数,在运行时进行类型检查,你的生活会好得多。问问你自己,通用版有什么给你买的吗?很少,以使您的代码完全无法理解的巨大代价。如果我们明天早上阅读方法签名,包括您自己和我自己在内的任何人都无法理解它。它比正则表达式差得多。

    【讨论】:

    • 我敢打赌,f 和 source 中使用的通配符捕获 (&lt;? extends SK, ? extends SV&gt;) 不匹配:-/
    • 我花了一段时间,但现在我想我已经有了洞察力! :-) 关键是捕获只会在外部类型级别完成,但这里的通配符实际上是签名的一部分。
    【解决方案3】:

    我觉得你应该看看Google Guava API。

    在那里你可以找到一个类似于你的转换接口的Function 接口。还有一个类Maps 具有用于创建或转换地图实例的实用方法。

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

    【讨论】:

    • 我已经看到 Guava 做了什么,他们有一个单独的地图转换类......这不是很优雅,但我想这是你可以使用的 Java 泛型限制。跨度>
    • 番石榴+1。你在这里重新发明轮子。 Guava 有适合您需求的方法和类
    • @Shervin 我已经知道我在重新发明轮子,但这是一个很好的练习,可以更流畅地使用泛型语义。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-03-11
    • 1970-01-01
    • 1970-01-01
    • 2011-12-13
    • 2020-08-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多