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

发布于 2024-10-14 15:48:33 字数 4691 浏览 2 评论 0原文

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

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

I decided to write some common Higher Order Functions in Java (map, filter, reduce, etc.) that are type safe via generics, and I'm having problems with wildcards matching in one particular function.

Just to be complete, the functor interface is this:

/**
 * 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);
}

The troubling function is like a map, but instead of transforming a Collection it transforms a Map (at first I thought it should be called mapMap, but it sounded so stupid that I ended up calling it remapEntries).

My first version was (and take a sit, because the signature is quite a monster):

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

And it seems to be quite correct, but the problem is that the transformation used must match exactly the type parameters, making difficult to reuse map functions for types that are compatible. So I decided to add wildcards to the signature, and it ended up like this:

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

But when I'm trying to test it, wildcard matching fails:

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

The error is:

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>

So, any hints on how to fix this? Or should I give up and write explicit loops for this? ^_^

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

夜唯美灬不弃 2024-10-21 15:48:33

我认为您应该查看 Google Guava API

您可以在那里找到函数 界面类似于您的转换界面。还有一个地图 使用实用方法来创建或转换地图实例。

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

I think you should take a look to Google Guava API.

There you can find a Function interface similar to your Transformation one. There is also a class Maps with utility methods to create or transform map instances.

You should also consider PECS when implementing methods for generics use.

橘和柠 2024-10-21 15:48:33

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

首先要修复的是 swap 的类型。输入类型不应该是 Entry,因为这样它就不能接受 Entry,它不是 EE的子类型。 S,N>。然而,EE。所以我们的变压器应该将其作为输入。对于输出,没有通配符,因为变压器无论如何只能实例化一个具体类型。我们只是想诚实准确地了解可以消耗什么以及将产生什么:

    /*     */ 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 是最终的,没有人扩展它,但我担心通用系统并没有那么聪明地知道那么,原则上,我做了 ?无论如何,都会扩展 String,以便以后使用。

然后,让我们考虑一下remapEntries()。由于我们提出的理由,我们怀疑传递给它的大多数转换器将具有与交换类似的类型声明。所以我们最好必须

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。但您似乎希望返回类型与调用者上下文中的 result 类型相同。我只想将返回类型设置为 void - 已经有足够多的麻烦了。

如果 swap 不使用 ,这个东西就会失败?扩展。例如,如果输入类型是String-Integer,那么执行就很荒谬?扩展了它们的。但是您可以使用具有不同参数类型声明的重载方法来匹配这种情况。

好吧,这成功了,纯粹是运气好。但是,这完全值得。如果你忘记它,并使用原始类型,用英语记录参数,在运行时进行类型检查,你的生活会好得多。问问自己,通用版本能给你带来什么吗?很少,但代价却是让你的代码完全无法理解。如果我们明天早上阅读方法签名,没有人,包括你自己和我自己,能够理解它。它比正则表达式糟糕得多。

This is a difficult one. The following knowledge is totally useless and nobody should care to posses:

First thing to fix is the type of swap. The input type should not be Entry<String,Number>, because then it cannot accept Entry<String,Integer>, which is not a subtype of E<S,N>. However, E<S,I> is a subtype of E<? extends S,? extends N>. So our transformer should take that as input. For the output, no wild card, because the transformer can only instantiate a concrete type anyway. We just want to be honest and accurate of what can be consumed and what will be produced:

    /*     */ 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()
            );
        }
    };

Note String is final and nobody extends it, but I'm afraid the generic system isn't that smart to know that, so as a matter of principle, I did ? extends String anyway, for later good.

Then, let's think about remapEntries(). We suspect that most transformers pass to it will have similar type declaration as the swap, because of the justifications we laid out. So we better have

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

to properly match that argument. From there, we work out the type of source and result, we want them to be as general as possible:

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 isn't necessary, it's fine to use directly Map<? super RK, ? super RV>. But it seems that you want the return type identical to the result type in caller's context. I woulda simply made the return type void - there is enough trouble already.

This thing will fail, if swap does not use ? extends. For example if the input type is String-Integer, it's ridiculous to do ? extends of them. But you can have a overloading method with different parameter type declaration to match this case.

Ok, that worked, out of sheer luck. But, it is totally not worth it. Your life is much better if you just forget about it, and use raw type, document the parameters in English, do type check at runtime. Ask yourself, does the generic version buy you anything? Very little, at the huge price of rendering your code completely incomprehensible. Nobody, including yourself, and myself, could make sense of it if we read the method signature tomorrow morning. It is much much worse than regex.

天气好吗我好吗 2024-10-21 15:48:33

我突然想到了一些事情:如果嵌套泛型参数中的通配符不会被捕获,因为它们实际上是类型的一部分,那么我可以在映射中使用反向边界,而不是在 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 接口的不可变包装器,如果调用 setValue 方法,该包装器会抛出异常,以确保至少运行时类型安全。

或者只是创建一个显式的不可变 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;
}

Something has just suddenly popped into my head: if the wildcards in nested generic parameters won't be captured as they are literally part of the type, then I could use the reverse bounds in the maps instead of using them in the 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;
}

The only problem is that we have to do the unchecked cast in the Transformation.apply. It would be totally safe if the Map.Entry interface were read-only, so we can just cross fingers and hope that the transformation does not try to call Map.Entry.setValue.

We could still pass an immutable wrapper of the Map.Entry interface that threw an exception if the setValue method was called to ensure at least runtime type safety.

Or just make an explicit immutable Entry interface and use it, but that's a little bit like cheating (as having two different Transformations):

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;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文